J_Knight_ iOS 高级面试题 基础题解答

4,963 阅读14分钟

最近从公司离职了,准备了一下接下来的面试,翻出了J_Knight的高级面试题复习了一下面试

iOS 基础题

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

区别: extension在编译期决议 它伴随类的产生而产生,亦随之一起消亡 category则完全不一样,它是在运行期决议的 extension可以添加实例变量,而category是无法添加实例变量的(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)。

分类应用 可以把类的实现分开在几个不同的文件里面。这样做有几个显而易见的好处,

1:可以减少单个文件的体积

2:可以把不同的功能组织到不同的category里

3:可以由多个开发者共同完成一个类

4:可以按需加载想要的category 等等。

5:声明私有方法

扩展应用 extension一般用来隐藏类的私有信息

struct category_t {
    constchar*name;//类的名字(name)
    classref_t cls;//类(cls)
    struct method_list_t *instanceMethods; //category中所有给类添加的实例方法的列表(instanceMethods)
    structmethod_list_t *classMethods;//category中所有添加的类方法的列表(classMethods)
    structprotocol_list_t *protocols; //category实现的所有协议的列表(protocols)
    structproperty_list_t *instanceProperties;//category中添加的所有属性(instanceProperties)
};

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

atomic只是保证了getter和setter存取方法的线程安全,并不能保证整个对象是线程安全的,因此在多线程编程时,线程安全还需要开发者自己来处理.关 于选择:atomic系统生成的getter、setter会保证get、set操作的安全性,但相对nonatomic来说,atomic要更耗费资源,且速度要慢,故在iPhone等小型设备上,如果没有多线程之间的通讯,使用nonatomic是更好的选 atomic系统自动生成的getter/setter方法会进行加锁操作 nonatomic系统自动生成的getter/setter方法不会进行加锁操作

例如:线程1调用了某一属性的setter方法并进行到了一半,线程2调用其getter方法,那么会执行完setter操作后,在执行getter操作,线程2会获取到线程1 setter后的完整的值. 当几个线程同时调用同一属性的setter、getter方法时,会get到一个完整的值,但get到的值不可控.例如:线程1 调用getter线程2 调用setter线程3 调用setter这3个线程并行同时开始,线程1会get到一个值,但是这个值不可控,可能是线程2,线程3 set之前的原始值,可能是线程2set的值,也可能是线程3 set的值

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

https://blog.csdn.net/future_one/article/details/81606895 。 浅谈iOS之weak底层实现原理
https://www.jianshu.com/p/f331bd5ce8f8  浅谈iOS之weak底层实现原理

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

1.2 如何关联对象
runtime提供了給我们3个API以管理关联对象(存储、获取、移除):
123456 //关联对象void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)//获取关联的对象id objc_getAssociatedObject(id object, const void *key)//移除关联的对象void objc_removeAssociatedObjects(id object)
其中的参数
id object:被关联的对象
const void *key:关联的key,要求唯一
            id value:关联的对象
objc_AssociationPolicy policy:内存管理的策略

void objc_removeAssociatedObjects(id object);,会移除所有的关联,包括其他模块添加的,因此应该用 objc_setAssociatedObject(..,nil,..) 的方式去卸载。
苹果官方文档说 OBJC_ASSOCIATION_ASSIGN 相当于一个 weak reference,但其实等于 assign/unsafe_unretained。
对于与weak的区别不在本文讨论范围内,浅显的区别在于变量释放后,weak 会把引用置空,unsafe_unretained会保留内存地址,一旦获取可能会野指针闪退。

 详情:https://www.jianshu.com/p/1feae48a5dda        AssociatedObject关联对象原理实现

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

KVO是通过isa-swizzling技术实现的(这句话是整个KVO实现的重点)。
在运行时根据原类创建一个中间类,这个中间类是原类的子类,
并动态修改当前对象的isa指向中间类。并且将class方法重写,返回原类的
Class。

1.在运行的时候创建被监类的子类

2.在子类中重写父类属性的set方法(故kvo之监听属性)

3注册这个子类

4.修改当前被监听的isa指针指向子类

5.实现set函数

在分析KVO的内部实现之前,先来分析一下KVO的存储结构,主要用到了以下几个类:
GSKVOInfo
GSKVOPathInfo
GSKVOObservation
@interface  GSKVOInfo : NSObject
{
  NSObject          *instance;  // Not retained.   observer保存观察者 注意这里也是 Not retained   释放之后,在调用会崩溃,需要在对象销毁前,移除所有观察者
  GSLazyRecursiveLock           *iLock;
  NSMapTable            *paths;   paths 用于保存keyPath 到 GSKVOPathInfo 的映射:
}
@interface  GSKVOPathInfo : NSObject
{
@public
  unsigned              recursion;
  unsigned              allOptions;            保存了观察者的options集合
  NSMutableArray        *observations;  保存了所有的观察者(GSKVOObservation 类型)
  NSMutableDictionary   *change;    保存了KVO触发要传递的内容
}

@interface  GSKVOObservation : NSObject
{
@public
  NSObject      *observer;      // Not retained (zeroing weak pointer)
  void          *context; 都是添加观察者时传入的参数
  int           options;  都是添加观察者时传入的参数
}
@end

KVO内部多次用到了KVC
1️⃣ 重写 setValue:forKey
2️⃣ 使用valueForKey --- valueForKeyPath获取属性的值,尤其是在使用点语法的时候,只有valueForKeyPath可以获得深层次的属性值。
所以KVO是基于KVC而实现的。

详细链接:https://www.jianshu.com/p/d6e4ba25acd2

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

Autorelease pool的实现原理

Autorelease pool是有objc_autoreleasePoolpush和objc_autoreleasePoolpop实现

objc_autoreleasePoolpush和objc_autoreleasePoolpop是由AutoreleasePoolPage 实现的

class AutoreleasePoolPage {   //大小4096 字节字节 双向列列表实现的
magic_t const magic; 用于对当前 AutoreleasePoolPage 完整性的校验
id *next; 指向了下一个为空的内存地址
pthread_t const thread; /当前所在的线程
AutoreleasePoolPage * const parent; 头节点
 AutoreleasePoolPage *child;    尾节点 。 
uint32_t const depth;   深度
uint32_t hiwat; 
POOL_SENTINEL(哨兵对象)
id next  next 指向了下一个为空的内存地址,如果 next 指向的地址加入一个 object
};

在每个自动释放池初始化调用 objc_autoreleasePoolPush 的时候,都会把一个 POOL_SENTINEL push 到自动释放池的栈顶,并且返回这个 POOL_SENTINEL 哨兵对象。
而当方法 objc_autoreleasePoolPop 调用时,就会向自动释放池中的对象发送 release 消息,直到第一个 POOL_SENTINEL:

注意:在这里会进入一个比较关键的方法 autoreleaseFast,并传入哨兵对象 POOL_SENTINEL:
void *objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage::push();
}
static inline void *push() {
    return autoreleaseFast(POOL_SENTINEL);
}
static inline id *autoreleaseFast(id obj) { 
AutoreleasePoolPage *page = hotPage(); hotPage 可以理解为当前正在使用的 AutoreleasePoolPage。
if (page && !page->full()) { return page->add(obj); }   有 hotPage 并且当前 page 不满,有 hotPage 并且当前 page 不满   调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中 
else if (page) { 
return autoreleaseFullPage(obj, page);  有 hotPage 并且当前 page 已满,调用 autoreleaseFullPage 初始化一个新的页,调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
} else { 
return autoreleaseNoPage(obj); } 无 hotPage 调用 autoreleaseNoPage 创建一个 hotPage
 }
id *add(id obj) {
    id *ret = next;
    *next = obj;
    next++;
    return ret;
}
这个方法其实就是一个压栈的操作,将对象加入 AutoreleasePoolPage 然后移动栈顶的指针
它会从传入的 page 开始遍历整个双向链表,直到:
查找到一个未满的 AutoreleasePoolPage
使用构造器传入 parent 创建一个新的 AutoreleasePoolPage
在查找到一个可以使用的 AutoreleasePoolPage 之后,会将该页面标记成 hotPage,然后调动上面分析过的 page->add 方法添加对象。

既然当前内存中不存在 AutoreleasePoolPage,就要从头开始构建这个自动释放池的双向链表,也就是说,新的 AutoreleasePoolPage 是没有 parent 指针的。
初始化之后,将当前页标记为 hotPage,然后会先向这个 page 中添加一个 POOL_SENTINEL 对象,来确保在 pop 调用的时候,不会出现异常。
最后,将 obj 添加到自动释放池中。

4.3 objc_autoreleasePoolPop 方法
static inline void pop(void *token) { 
AutoreleasePoolPage *page = pageForPointer(token); //使用 pageForPointer 获取当前 token 所在的 AutoreleasePoolPage

static AutoreleasePoolPage *pageForPointer(const void *p) { 
return pageForPointer((uintptr_t)p);
 } 
static AutoreleasePoolPage *pageForPointer(uintptr_t p) {
 AutoreleasePoolPage *result; 
uintptr_t offset = p % SIZE; 
assert(offset >= sizeof(AutoreleasePoolPage)); 
result = (AutoreleasePoolPage *)(p - offset); result->fastcheck(); 
return result;
 }
pageForPointer 方法主要是通过内存地址的操作,获取当前指针所在页的首地址 将指针与页面的大小,也就是 4096 取模,得到当前指针的偏移量,因为所有的 AutoreleasePoolPage 在内存中都是对齐的 而最后调用的方法 fastCheck() 用来检查当前的 result 是不是一个 AutoreleasePoolPage。
id *stop = (id *)token; 
page->releaseUntil(stop);  // 调用 releaseUntil 方法释放栈中的对象,直到 stop  //它的实现还是很容易的,用一个 while 循环持续释放 AutoreleasePoolPage 中的内容,直到 next 指向了 stop。
if (page->child) { 
if (page->lessThanHalfFull()) { 
  page->child->kill();  //调用 child 的 kill 方法
} else if (page->child->child)
 { 
 page->child->child->kill(); 
} } }

整个自动释放池 autoreleasepool 的实现以及 autorelease 方法都已经分析完了,我们再来回顾一下文章中的一些内容:
自动释放池是由 AutoreleasePoolPage 以双向链表的方式实现的。
当对象调用 autorelease 方法时,会将对象加入 AutoreleasePoolPage 的栈中。
调用 AutoreleasePoolPage::pop 方法会向栈中的对象发送 release 消息。

作者:卡布达巨人
链接:https://juejin.cn/post/1
来源:掘金

讲一下对象,类对象,元类,跟元类结构体的组成以及他们是如何相关联的?为什么对象方法没有保存的对象结构体里,而是保存在类对象的结构体里?

www.cnblogs.com/wsnb/p/6163…

class_ro_t 和 class_rw_t 的区别?

class_rw_t 和 class_ro_t
ObjC 类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t 中:
struct class_rw_t {
    uint32_t flags;
    uint32_t version;
    const class_ro_t *ro;
    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    Class firstSubclass;
    Class nextSiblingClass;
};
其中还有一个指向常量的指针 class_ro_t,其中存储了当前类在编译期就已经确定的属性、方法以及遵循的协议。
struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
    uint32_t reserved;
    const uint8_t * ivarLayout;
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
};
在编译期间类的结构中的 class_data_bits_t *data 指向的是一个 class_ro_t * 指针:

isa 是指向元类的指针,不了解元类的可以看:Classes and Metaclasses
super_class 指向当前类的父类
cache 用于缓存指针和 vtable,加速方法的调用
bits 就是存储类的方法、属性、遵循的协议等信息的地方

然后在加载 ObjC 运行时的过程中在 realizeClass 方法中:
从 class_data_bits_t 调用 data 方法,将结果从 class_rw_t 强制转换为 class_ro_t 指针
初始化一个 class_rw_t 结构体
设置结构体 ro 的值以及 flag
最后设置正确的 data。

但是,在这段代码运行之后 class_rw_t 中的方法,属性以及协议列表均为空。这时需要 realizeClass 调用 methodizeClass 方法来将类自己实现的方法(包括分类)、属性和遵循的协议加载到 methods、 properties 和 protocols 列表中。


详情:https://blog.csdn.net/fishmai/article/details/71157861

iOS 中内省的几个方法?class方法和objc_getClass方法有什么区别?

Class objc_getClass(const chat *aClassName)
1:Class objc_getClass(const chat *aClassName)
1> 传入字符串类名
2> 返回对应的类对象

Class object_getClass(id obj)
2. Class object_getClass(id obj)
1> 传入的obj可能是instance对象,class对象、meta-class对象
2> 返回值
a:如果是instance对象,返回class对象
b:如果是class对象,返回meta-class对象
c:如果是meta-class对象,返回NSObject(基类)的meta-class对象

- (class)class、+(class)class
3:- (class)class、+(class)class
1>返回的就是类对象

结论:当obj为实例变量时,object_getClass(obj)与[obj class]输出结果一直,均获得isa指针,即指向类对象的指针。
总结:经上面初步的探索得知,object_getClass(obj)返回的是obj中的isa指针;而[obj class]则分两种情况:一是当obj为实例对象时,[obj class]中class是实例方法:- (Class)class,返回的obj对象中的isa指针;二是当obj为类对象(包括元类和根类以及根元类)时,调用的是类方法:+ (Class)class,返回的结果为其本身。

作者:洲洲哥
链接:https://www.jianshu.com/p/5cfd52d222f0
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

在运行时创建类的方法objc_allocateClassPair的方法名尾部为什么是pair(成对的意思)

 动态创建类
动态创建类涉及到以下几个函数:
12345678 // 创建一个新类和元类Class objc_allocateClassPair ( Class superclass, const char *name, size_t extraBytes );// 销毁一个类及其相关联的类void objc_disposeClassPair ( Class cls );// 在应用中注册由objc_allocateClassPair创建的类void objc_registerClassPair ( Class cls );
objc_allocateClassPair函数:如果我们要创建一个根类,则superclass指定为Nil。extraBytes通常指定为0,该参数是分配给类和元类对象尾部的索引ivars的字节数。
为了创建一个新类,我们需要调用objc_allocateClassPair。然后使用诸如class_addMethod,class_addIvar等函数来为新创建的类添加方法、实例变量和属性等。完成这些后,我们需要调用objc_registerClassPair函数来注册类,之后这个新类就可以在程序中使用了。
实例方法和实例变量应该添加到类自身上,而类方法应该添加到类的元类上。
objc_disposeClassPair函数用于销毁一个类,不过需要注意的是,如果程序运行中还存在类或其子类的实例,则不能调用针对类调用该方法。

http://www.cocoachina.com/ios/20141031/10105.html
https://blog.csdn.net/hypercode/article/details/53931517

一个int变量被__block修饰与否的区别?

 Block的本质<一>
 http://www.cocoachina.com/ios/20180910/24846.html
 理清 Block 底层结构及其捕获行为  https://juejin.cn/post/6844903686615859213

 iOS底层原理总结 - 探寻block的本质(一) http://www.cocoachina.com/ios/20180628/23965.html
 iOS底层原理总结 - 探寻block的本质(二) http://www.cocoachina.com/ios/20180628/23968.html


 iOS 看懂此文,你的block再也不需要WeakSelf弱引用了! http://www.cocoachina.com/ios/20180110/21817.html
 iOS中Block的用法,举例,解析与底层原理(这可能是最详细的Block解析 .  http://www.cocoachina.com/ios/20180424/23147.html

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

关于使用__weak和__strong
    大家都看到别人在block里面使用self或者self的属性的时候要使用__weak修饰self,然后才能block里面使用,在block里面使用的时候又将weakSelf使用__strong修饰进行使用,比如:
__weak __typeof(self) weakSelf  = self;
self.block = ^{
    __strong __typeof(self) strongSelf = weakSelf; 
    [strongSelf doSomeThing];
    [strongSelf doOtherThing];
};
为什么使用weakSelf
    通过 clang -rewrite-objc 源代码文件名 将代码转为c++代码(实质是c代码),可以看到block是一个结构体,它会将全局变量保存为一个属性(是__strong的),而self强引用了block这会造成循环 引用。所以需要使用__weak修饰的weakSelf。
为什么在block里面需要使用strongSelf
     是为了保证block执行完毕之前self不会被释放,执行完毕的时候再释放。这时候会发现为什么在block外边使用了__weak修饰self,里面使用__strong修饰weakSelf的时候不会发生循环引用?!
    PS:strongSelf只是为了保证在block内部执行的时候不会释放,但存在执行前self就已经被释放的情况,导致strongSelf=nil。注意判空处理。
不会引起循环引用的原因
    因为block截获self之后self属于block结构体中的一个由__strong修饰的属性会强引用self, 所以需要使用__weak修饰的weakSelf防止循环引用。
    block使用的__strong修饰的weakSelf是为了在block(可以理解为函数)生命周期中self不会提前释放。strongSelf实质是一个局部变量(在block这个“函数”里面的局部变量),当block执行完毕就会释放自动变量strongSelf,不会对self进行一直进行强引用。
总结
    外部使用了weakSelf,里面使用strongSelf却不会造成循环,究其原因就是因为weakSelf是block截获的属性,而strongSelf是一个局部变量会在“函数”执行完释放。

RunLoop的作用是什么?它的内部工作机制了解么?(最好结合线程和内存管理来说)

https://juejin.cn/post/6844903604965523464     iOS底层原理探究-Runloop  
https://juejin.cn/post/6844903606932471822   RunLoop终极解析:输入源,定时源,观察者,线程间通信,端口通信,NSPort,NSMessagePort,NSMachPort,NSPortMessage             

https://juejin.cn/post/6844903598350925831   iOS底层原理总结 - RunLoop   
https://juejin.cn/post/1fc3ec8a0bb9f0065bd2889  iOS RunLoop 探究 

http://www.cocoachina.com/ios/20180814/24550.html  老司机出品——源码解析之RunLoop详解
https://juejin.cn/post/6844903588712415239     iOS RunLoop详解  
http://www.cocoachina.com/ios/20180522/23447.html   iOS开发·RunLoop源码与用法完全解析

http://www.cocoachina.com/ios/20180626/23932.html 深入理解RunLoop

哪些场景可以触发离屏渲染?(知道多少说多少)

  • shouldRasterize(光栅化)
  • masks(遮罩)
  • shadows(阴影)
  • edge antialiasing(抗锯齿)
  • group opacity(不透明)
  • 复杂形状设置圆角等
  • 渐变

作者:J_Knight_ 链接:juejin.cn/post/684490… 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。