iOS集合面经

iOS开发需要掌握的原理

2020-03-12  本文已影响0人  麦子_KB

目录:
1.Runtime
2.NSNotification相关
3.RunLoop
4.多线程相关
5.KVO
6.Block相关
7.视图与图像相关
8.数据结构与算法
9.架构设计
10.系统基础知识
11.性能优化相关

一、Runtime

1.介绍下runtime的内存模型(isa、对象、类、metaclass、结构体的存储信息等)?
结构模型图
/** p/x &(obj->isa): 0x0000600001f2c780
  *  p obj:           0x0000600001f2c780
  *  实例对象,isa指针地址为实例对象obj的地址,但isa指向的是类对象的地址    
  */
NSObject *obj = [[NSObject alloc] init];
/** 类对象,一个类对象在整个程序运行过程中只会存在一个,不管你调用多少次class方法,同一个类返回的类对象地址始终是一样的类对象结构体
        struct objc_class {
            Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

            #if !__OBJC2__
                Class _Nullable super_class                              OBJC2_UNAVAILABLE;
                const char * _Nonnull name                               OBJC2_UNAVAILABLE;
                long version                                             OBJC2_UNAVAILABLE;
                long info                                                OBJC2_UNAVAILABLE;
                long instance_size                                       OBJC2_UNAVAILABLE;
                struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
                struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
                struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
                struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
            #endif
        } OBJC2_UNAVAILABLE;
    */
Class cls = [NSObject class];
//元类对象,元类对象需要通过object_getClass这个runtime函数获取,在整个程序运行过程中也只存在一个该类的元类对象
Class metaCls = object_getClass(cls);
NSLog(@"obj->%p",obj);
NSLog(@"cls->%p",cls);
NSLog(@"metaCls->%p",metaCls);
  1. instance中存放着isa和所有对象的值,instance的isa指向的是class。
  2. class存放着isa、superclass以及NSObject的属性信息和对象方法。superclass指向其父类,NSObject(Root class)的类对象的superclass指向nil。isa指向的是meta。通过对象调用对象方法的时候,首先会通过instance的isa找到其class,然后在class中找到对象方法并调用,没有找到则会通过superclass一层一层往上找,最终还是找不到的时候就会报错。
  3. meta和class类似,只不过它存储的不是对象方法,而是类方法。并且元类的 isa 全部指向NSObject的元类对象。元类的superclass指向其父类,但是NSObject的元类对象的superclass指向的是其类对象。通过类对象调用类方法的时候,首先会通过class的isa找到其meta,然后在meta中找到类方法并调用,没有找到则会通过superclass一层一层往上找,最终还是找不到的时候就会报错。

艾欧艾斯之手写了一个不错的方法来验证,其实原理是消息转发的一部分:

@interface NSObject (DVObject)
@end
@implementation NSObject (DVObject)
- (void)eat {
    NSLog(@"吃饭");
}
@end

@interface Person : NSObject
+ (void)eat;
@end
@implementation Person
@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [Person eat];
    }
    return 0;
}

运行这段代码,我们发现程序并没有奔溃,而是打印了”吃饭“,这是为什么呢?其实离不开oc的运行时特性。
这里Person调用eat类方法,首先会通过isa找到自己的元类对象,发现没有eat方法,然后通过superclass找到NSObject的元类对象继续查找eat方法,依然没有,再通过superclass,注意,此时的superclass指向的是NSObject的类对象,而通过代码可以发现NSObject的类对象中是存在eat方法的,并且调用成功。

SEL (@selector(someMethod)) 有以下的特性:

  1. Objective-C 为我们维护了一个巨大的选择子表
  2. 在使用 @selector() 时会从这个选择子表中根据选择子的名字查找对应的 SEL。如果没有找到,则会生成一个 SEL 并添加到表中
  3. 在编译期间会扫描全部的头文件和实现文件将其中的方法以及使用 @selector() 生成的选择子加入到选择子表中
3.category如何被加载的,两个category的load方法的加载顺序? 两个category的同名方法的加载顺序?

在runtime时,首先把category的实例方法、协议以及属性添加到类上,其次
把category的类方法和协议添加到类的metaclass上;
附加category到类的工作会先于+load方法的执行,+load的执行顺序是先类,后category,而category的+load执行顺序是根据编译顺序决定的;
我们已经知道category其实并不是完全替换掉原来类的同名方法,只是category在方法列表的前面而已,对于category同名方法的加载顺序,显然是按照后编译的方法先执行;
对于调用到原来类中被category覆盖掉的方法,我们只要顺着方法列表找到最后一个对应名字的方法,就可以调用原来类的方法。
点击这里查看详细介绍

4.category & extension区别,能给NSObject添加Extension吗,结果如何?

category:分类
给类添加新的方法
不能给类添加成员变量
通过@property定义的变量,只能生成对应的getter和setter的方法声明,但是不能实现getter和setter方法,同时也不能生成带下划线的成员属性;
注意:为什么不能添加属性,原因就是category是运行期决定的,在运行期类的内存布局已经确定,如果添加实例变量会破坏类的内存布局,会产生意想不到的错误。
extension:扩展
可以给类添加成员变量,但是是私有的,可以給类添加方法,但是是私有的
添加的属性和方法是类的一部分,在编译期就决定的。
在编译器和头文件的@interface和实现文件里的@implement一起形成了一个完整的类。伴随着类的产生而产生,也随着类的消失而消失,必须有类的源码才可以给类添加extension,所以对于系统一些类,如nsstring,就无法添加类扩展
不能给NSObject添加Extension,因为在extension中添加的方法或属性必须在源类的文件的.m文件中实现才可以,即:你必须有一个类的源码才能添加一个类的extension。
扩展是在编译期决定,而分类是在运行期。

5.IMP、SEL、Method的区别和使用场景?
IMP:是方法的实现,即:一段c函数
SEL:是方法名
Method:是objc_method类型指针,它是一个结构体,如下:
struct objc_method {
    SEL _Nonnull method_name        ///方法的名称
    char * _Nullable method_types   ///方法的类型
    IMP _Nonnull method_imp         ///方法的具体实现,由 IMP 指针指向
} 
6.load、initialize方法的区别什么?在继承关系中他们有什么区别?

+load方法在这个文件被程序装载时调用。只要是在Compile Sources中出现的文件总是会被装载,这与这个类是否被用到无关,因此+load方法总是在main函数之前调用。调用方式并不是采用runtime的objc_msgSend方式调用的,而是直接采用函数的内存地址直接调用的; 多个类的load调用顺序,是依赖于compile sources中的文件顺序决定的,根据文件从上到下的顺序调用子类和父类同时实现load的方法时,父类的方法先被调用本类与category的调用顺序是,优先调用本类的(注意:category是在最后被装载的);load是被动调用的,在类装载时调用的,不需要手动触发调用
initialize:当类或子类第一次收到消息时被调用(即:静态方法或实例方法第一次被调用,也就是这个类第一次被用到的时候),方式是通过runtime的objc_msgSend的方式调用的,此时所有的类都已经装载完毕;子类和父类同时实现initialize,父类的先被调用,然后调用子类的;本类与category同时实现initialize,category会覆盖本类的方法,只调用category的initialize一次(这也说明initialize的调用方式采用objc_msgSend的方式调用的);initialize是主动调用的,只有当类第一次被用到的时候才会触发。
可参考详细解释

7.weak的实现原理?SideTable的结构是什么样的?

weak表其实是一个hash表,Key是所指对象的地址,Value是weak指针的地址数组,weak是弱引用,所引用对象的计数器不会+1,并在引用对象被释放的时候自动被设置为nil。通常用于解决循环引用问题。

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

1、调用objc_release
2、因为对象的引用计数为0,所以执行dealloc
3、在dealloc中,调用了_objc_rootDealloc函数
4、在_objc_rootDealloc中,调用了object_dispose函数
5、调用objc_destructInstance
6、最后调用objc_clear_deallocating,详细过程如下:
   a. 从weak表中获取废弃对象的地址为键值的记录
   b. 将包含在记录中的所有附有 weak修饰符变量的地址,赋值为nil
   c. 将weak表中该记录删除
   d. 从引用计数表中删除废弃对象的地址为键值的记录

点击查看详细介绍

8.系统如何实现关联对象的?关联对象如何进行内存管理的?关联对象的应用?

可以在不改变类的源码的情况下,为类添加实例变量。需要注意的是,这里指的实例变量,并不是真正的属于类的实例变量,而是一个关联值变量。

- (void)setAssociatedObject:(id)object {
    objc_setAssociatedObject(self, @selector(associatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
结构图: 关联对象结构
  1. 关联对象的值实际上是通过AssociationsManager对象负责管理的。在AssociationsManager中有一个spinlock类型的自旋锁lock。保证每次只有一个线程对AssociationsManager进行操作,保证线程安全。
  2. 关联哈希表AssociationsHashMap中的key为object地址的指针,value为对象关联表ObjcAssociationMap的首地址,在ObjcAssociationMap表中,key值是set方法里面传过来的形参const void *key,value值是ObjcAssociation对象。 ObjcAssociation对象中存储了set方法最后两个参数,policy和value
  3. 先初始化一个 AssociationsManager,获取唯一的保存关联对象的哈希表 AssociationsHashMap,然后在AssociationsHashMap里面去查找object地址的指针。如果找到,就找到了第二张表ObjectAssociationMap。在这张表里继续查找key。如果在第二张表ObjectAssociationMap找到对应的ObjcAssociation对象,那就更新它的值。如果没有找到,就新建一个ObjcAssociation对象,放入第二张表ObjectAssociationMap中。

关联对象注意的点:

  1. 由于不改变原类的实现,所以可以给原生类或者是打包的库进行扩展,一般配合Category实现完整的功能。
  2. ObjC类定义的变量,由于runtime的特性,都会暴露到外部,使用关联对象可以隐藏关键变量,保证安全。
  3. 可以用于KVO,使用关联对象作为观察者,可以避免观察自身导致循环。
  4. 这里需要注意的是标记成OBJC_ASSOCIATION_ASSIGN的关联对象和@property (weak) 是不一样的,上面表格中等价定义写的是 @property (unsafe_unretained),对象被销毁时,属性值仍然还在。如果之后再次使用该对象就会导致程序闪退。所以我们在使用OBJC_ASSOCIATION_ASSIGN时,要格外注意
  5. 关于关联对象还有一点需要说明的是objc_removeAssociatedObjects。这个方法是移除源对象中所有的关联对象,并不是其中之一。所以其方法参数中也没有传入指定的key。要删除指定的关联对象,使用 objc_setAssociatedObject 方法将 key 对应的value设置成 nil 即可。objc_setAssociatedObject(self, associatedKey, nil, OBJC_ASSOCIATION_COPY_NONATOMIC);
  6. 关联对象的释放问题:关联对象要比本对象释放晚一些。
    调用 object_dispose()
    为 C++ 的实例变量们(iVars)调用 destructors
    为 ARC 状态下的 实例变量们(iVars) 调用 -release
    解除所有使用 runtime Associate方法关联的对象
    解除所有 __weak 引用
9.ARC的实现原理?AutoreleasePool原理

详细原理可参照这篇文章 点击查看

ARC下可能导致内存泄漏:
block中的循环引用
NSTimer的循环引用
addObserver的循环引用
delegate的强引用
大次数循环内存爆涨
非OC对象的内存处理(需手动释放)

10.class、objc_getClass、object_getclass 方法有什么区别?

objc_getClass参数是类名的字符串,返回的就是这个类的类对象;object_getClass参数是id类型,它返回的是这个id的isa指针所指向的Class,如果传参是Class,isa指针指向的是元类对象,即返回该Class的metaClass。

11.Method Swizzle注意事项?
  1. 第一个风险是,需要在 +load 方法中进行方法交换。因为如果在其他时候进行方法交换,难以保证另外一个线程中不会同时调用被交换的方法,从而导致程序不能按预期执行。
  2. 第二个风险是,被交换的方法必须是当前类的方法,不能是父类的方法,直接把父类的实现拷贝过来不会起作用。父类的方法必须在调用的时候使用,而不是方法交换时使用。
  3. 第三个风险是,交换的方法如果依赖了 cmd,那么交换后,如果 cmd 发生了变化,就会出现各种奇怪问题,而且这些问题还很难排查。特别是交换了系统方法,你无法保证系统方法内部是否依赖了 cmd。
  4. 第四个风险是,方法交换命名冲突。如果出现冲突,可能会导致方法交换失败
12.iOS是怎么管理内存的?

iOS采用自动应用计数来进行内存管理(ARC)。一个对象被创建出来后,被自己持有,此时引用计数为1,当其他变量持有该对象时,引用计数加1。当持有者调用release 时,引用计数减1。当对象的引用计数为0时,内存就会被释放回收。
在最开始的时候,程序是直接访问物理内存,但后来有了多程序多任务同时运行,就出现了很多问题。比如,同时运行的程序占用的总内存必须要小于实际物理内存大小。再比如,程序能够直接访问和修改物理内存,也就能够直接访问和修改其他程序所使用的物理内存,程序运行时的安全就无法保障

二、NSNotification相关

1.通知存储是以nameobject为维度的,即判定是不是同一个通知要从nameobject区分,如果他们都相同则认为是同一个通知,后面包括查找逻辑、删除逻辑都是以这两个为维度的;
2.理解数据结构的设计是整个通知机制的核心,其他功能只是在此基础上扩展了一些逻辑;
3.存储过程并没有做去重操作,这也解释了为什么同一个通知注册多次则响应多次;
4.NSNotificationCenter都是同步发送的,关于NSNotificationQueue的异步发送,从线程的角度看并不是真正的异步发送,或可称为延时发送,它是利用了runloop的时机来触发。
查看更多介绍

三、Runloop

17495317-90f472cc5d134bdb.png
1.App是如何响应触摸事件的?

1.APP进程的mach port接收来自SpringBoard的触摸事件,主线程的runloop被唤醒,触发source1回调。
2.source1回调又触发了一个source0回调,将接收到的IOHIDEvent对象封装成UIEvent对象,此时APP将正式开始对于触摸事件的响应。
3.source0回调将触摸事件添加到UIApplication的事件队列,当触摸事件出队后UIApplication为触摸事件寻找最佳响应者。
4.寻找到最佳响应者之后,接下来的事情便是事件在响应链中传递和响应。
更加详细介绍,请点击这里
或者参考我之前写的文章 iOS基础篇-事件处理

2.runloop与线程关系,为什么只在主线程刷新UI?

runloop与线程是一一对应的,一个runloop对应一个核心的线程,为什么说是核心的,是因为runloop是可以嵌套的,但是核心的只能有一个,他们的关系保存在一个全局的字典里。
runloop在第一次获取时被创建,在线程结束时被销毁。
对于主线程来说,runloop在程序一启动就默认创建好了。

对于子线程来说,runloop是懒加载的,只有当我们使用的时候才会创建,所以在子线程用定时器要注意:确保子线程的runloop被创建,不然定时器不会回调

主线程刷新UI是因为多线程技术有坑,所以 UIKit 干脆就做成了线程不安全,只能在主线程上操作

3.如何使线程保活?PerformSelector 的实现原理?

使用mach_port线程保活?线程之间的通讯是靠mach_port?

NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 RunLoop 不退出

当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

四、多线程相关

关于多线程基础介绍,看这篇文章就够了,点击查看

//OperationQueue 创建的自定义队列同时具有串行、并发功能
//NSInvocationOperation在swift中废弃
let ope1 = BlockOperation {
    for _ in 0..<2 {
       //模拟耗时操作
       Thread.sleep(forTimeInterval: 2)
       print("1--->",Thread.current)
    }
 }
let ope2 = BlockOperation {
    for _ in 0..<2 {
        //模拟耗时
        Thread.sleep(forTimeInterval: 2)
         print("2--->",Thread.current) 
     }
 }
let ope3 = BlockOperation {
     for _ in 0..<2 {
         //模拟耗时操作
         Thread.sleep(forTimeInterval: 2)
         print("3--->",Thread.current)
     }
}
let queue = OperationQueue.init()
queue.addOperation(ope1)
queue.addOperation(ope2)
queue.addOperation(ope3)
//当maxConcurrentOperationCount=-1时(默认值)并行
********************************************************************
2---> <NSThread: 0x6000036a9e80>{number = 3, name = (null)}
1---> <NSThread: 0x6000036ae200>{number = 5, name = (null)}
3---> <NSThread: 0x60000368a900>{number = 7, name = (null)}
1---> <NSThread: 0x6000036ae200>{number = 5, name = (null)}
3---> <NSThread: 0x60000368a900>{number = 7, name = (null)}
2---> <NSThread: 0x6000036a9e80>{number = 3, name = (null)}

//当设置maxConcurrentOperationCount=1时,变为串行队列
********************************************************************
1---> <NSThread: 0x6000003be380>{number = 4, name = (null)}
1---> <NSThread: 0x6000003be380>{number = 4, name = (null)}
2---> <NSThread: 0x6000003be380>{number = 4, name = (null)}
2---> <NSThread: 0x6000003be380>{number = 4, name = (null)}
3---> <NSThread: 0x60000035d8c0>{number = 7, name = (null)}
3---> <NSThread: 0x60000035d8c0>{number = 7, name = (null)}
1.多线程并发带来的内存问题?

我们知道,创建线程的过程,需要用到物理内存,CPU 也会消耗时间。而且,新建一个线程,系统还需要为这个进程空间分配一定的内存作为线程堆栈。堆栈大小是 4KB 的倍数。在 iOS 开发中,主线程堆栈大小是 1MB,新创建的子线程堆栈大小是 512KB。
除了内存开销外,线程创建得多了,CPU 在切换线程上下文时,还会更新寄存器,更新寄存器的时候需要寻址,而寻址的过程还会有较大的 CPU 消耗。所以,线程过多时内存和 CPU 都会有大量的消耗,从而导致 App 整体性能降低,使得用户体验变成差。CPU 和内存的使用超出系统限制时,甚至会造成系统强杀。这种情况对用户和 App 的伤害就更大了。

2.iOS有哪些类型的线程锁,分别介绍下作用和使用场景?

一共可以分为以下几类:

  1. 使用NSLock(普通锁;已实现NSLocking协议)实现锁
  2. 使用Synchronized指令实现锁
  3. 使用C语言的pthread_mutex_t实现锁
  4. 使用GCD的dispatch_semaphore_t(信号量)实现锁
  5. 使用NSRecursiveLock(递归锁;已实现NSLocking协议)实现锁;可以在递归场景中使用。如果使用NSLock,会出现死锁
  6. 使用NSConditionLock(条件锁;已实现NSLocking协议)实现锁;可以在需要符合条件才进行锁操作的场景中使用
  7. 使用NSDistributedLock(分布式锁;区别其他锁类型,它没有实现NSLocking协议)实现锁;它基于文件系统,会自动创建用于标识的临时文件或文件夹,执行完后自动清除临时文件或文件夹;可以在多个进程或多个程序之间需要构建互斥的场景中使用
    具体用法可以查看这里
3.dispatch_once实现原理?
原理图

首次调用dispatch_once时,因为外部传入的dispatch_once_t变量值为nil,故vval会为NULL,故if判断成立。然后调用_dispatch_client_callout执行block,然后在block执行完成之后将vval的值更新成DISPATCH_ONCE_DONE表示任务已完成。最后遍历链表的节点并调用_dispatch_thread_semaphore_signal来唤醒等待中的信号量;

当其他线程同时也调用dispatch_once时,因为if判断是原子性操作,故只有一个线程进入到if分支中,其他线程会进入else分支。在else分支中会判断block是否已完成,如果已完成则跳出循环;否则就是更新链表并调用_dispatch_thread_semaphore_wait阻塞线程,等待if分支中的block完成后再唤醒当前等待的线程。
参考这篇文章

4.线程和队列的关系?主线程和主队列?
  1. 主队列是系统默认为我们创建的DispatchQueue.main,它是一个串行队列;
  2. 主线程在程序启动的时候,系统会自动启动,并会加载在RunLoop上;
  3. 队列是运行在线程上的,二者本质上没有什么联系的,需要注意的是,sync 不会启动新的线程;
  4. 队列之间也平等的,系统默认会帮助我们分配两个队列dispatchMain()和DispatchQueue.global()

五、KVO相关

1.实现原理?如何手动关闭kvo?通过KVC修改属性会触发KVO么?
例如在使用KVO监听Person对象的name属性,runtime 动态的生成了一个 NSKVONotifying_Person子类 并重写了 setName、class、dealloc方法。 原理图

首先当我们改变p1.name 的值时 并不是首先执行的 setName: 这个方法 ,而是先调用了 willChangeValueForKey 其次 调用父类的 setter 方法 对属性赋值 ,然后再调用 didChangeValueForKey 方法 ,并在 didChangeValueForKey 内部 调用监听器的 observeValueForKeyPath方法 告诉外界 属性值发生了改变。
这篇文章分析的很详细 大兵布莱恩特·的文章

2.如何手动触发 kvo ? 如何手动关闭 kvo ?

在这里我创建了一个Person类,我们设置监听他的name属性变化

Person *p1 = [[Person alloc] init];
Person *p2 = [[Person alloc] init];
id cls1 = object_getClass(p1);
id cls2 = object_getClass(p2);
NSLog(@"添加 KVO 之前: cls1 = %@  cls2 = %@ ",cls1,cls2);
   
[p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
cls1 = object_getClass(p1);
cls2 = object_getClass(p2);
NSLog(@"添加 KVO 之后: cls1 = %@  cls2 = %@ ",cls1,cls2);
p1.name = @"jack";

///监听name的变化回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"触发了kvo");
    }
}
//手动触发kvo
[p1 willChangeValueForKey:@"name"];
[p1 didChangeValueForKey:@"name"];

注意:这两个方法需要同时实现,否则无法手动触发kvo

///Person.m中
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"name"]) {
        return YES;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}
这里有个注意的几点:
  1. 若开启了手动监听某个属性,则设置的手动关闭会失效的
  2. kvo是监听是以类对象为基础。self监听了p1,不管p1中是否存在name属性,都会创建NSKVONotifying_Person类,并重写它所有属性的setter方法。
  3. 忘记写监听回调方法 observeValueForKeyPath、add和remove次数不匹配时,会发生奔溃,在使用时需要特别注意!

六、Block相关

block完整原理,可参考这篇文章

1.block内部实现的结构是什么样的?

使用命令行将代码转化为c++查看其内部结构
clang main.m -rewrite-objc -o dest.cpp

int main(int argc, char * argv[]) {
    int value = 1;
    void (^test)(void) = ^(){
    };
    test();
}

编译后c++核心模块:

///__block_impl是编译器给我们生成的结构体,每一个block都会用到这个结构体
struct __main_block_impl_0 {
    struct __block_impl impl; ///__block_impl 变量impl
    struct __main_block_desc_0* Desc;
    int value;///我们声明的局部变量value
    
    ///结构体的构造函数
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0, int _value) {
        impl.isa = &_NSConcreteStackBlock; ///说明block是栈
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
///编译器根据block代码生成的全局态函数,会被赋值给impl.FuncPtr
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int value = __cself->value;
}
///编译器根据block代码生成的block描述,主要是记录下__main_block_impl_0结构体大小
static struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

int main(int argc, char * argv[]) {
    int value = 1;
    void (*test)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA), value);
    ((void (*)(__block_impl *)) ((__block_impl *)test)->FuncPtr)((__block_impl *)test);
}

从代码中可以看到,我们写在block块中的代码封装成__main_block_func_0函数,并将__main_block_func_0函数的地址传入了__main_block_impl_0的构造函数中保存在结构体内。

  1. __block_impl结构体中isa指针存储着&_NSConcreteStackBlock地址,可以暂时理解为其类对象地址,block就是_NSConcreteStackBlock类型的。
  2. block代码块中的代码被封装成__main_block_func_0函数,FuncPtr则存储着__main_block_func_0函数的地址。
  3. Desc指向__main_block_desc_0结构体对象,其中存储__main_block_impl_0结构体所占用的内存。
    总结:局部变量都会被block捕获,自动变量是值捕获,静态变量为地址捕获。全局变量则不会被block捕获

2.block是类么?有哪些类型?

block本质上也是一个oc对象,他内部也有一个isa指针。block是封装了函数调用以及函数调用环境的OC对象,本质上继承自NSObject。

block有3种类型:
NSGlobalBlock ( _NSConcreteGlobalBlock )类型的block,因为这样使用block并没有什么意义。
NSStackBlock ( _NSConcreteStackBlock )类型的block存放在栈中,我们知道栈中的内存由系统自动分配和释放,作用域执行完毕之后就会被立即释放,而在相同的作用域中定义block并且调用block似乎也多此一举。
NSMallocBlock ( _NSConcreteMallocBlock )NSMallocBlock是在平时编码过程中最常使用到的。存放在堆中需要我们自己进行内存管理。

局部变量都会被block捕获,自动变量(auto关键字是系统自动添加)是值捕获,静态变量为地址捕获。全局变量在哪里都可以访问 ,所以不用捕获

3.block的属性修饰词为什么是copy?block发生copy时机?

在MRC环境下调试,经常需要使用copy来保存block,将栈上的block拷贝到堆中,即使栈上的block被销毁,堆上的block也不会被销毁,需要我们自己调用release操作来销毁。而在ARC环境下回系统会自动copy,是block不会被销毁。在ARC环境下,编译器会根据情况自动将栈上的block进行一次copy操作,将block复制到堆上。
如果不copy的话,那么block就不会在堆空间上,无法对你生命周期进行控制。需要注意循环引用(ARC环境下 strong 、copy没有区别)

block发生copy时机?

  1. Block作为函数返回值返回时;
  2. 将Block赋值给附有__strong修饰符id类型的类或者Block类型成员变量时;
4.block在修改NSMutableArray,需不需要添加__block?

不需要。修改内容也是对数组的使用,只有对对象赋值的时候才需要__block。

5.解决循环引用时为什么要用__strong、__weak修饰?
__weak __typeof (self)weakSelf = self;

_observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"Change" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
    __strong __typeof(weakSelf)strongSelf = weakSelf;
    if (strongSelf) {
        [strongSelf reload];
    }
}
  1. 在block之前定义对self的弱引用weakSelf,因为是弱引用,所以self被释放时weakSelf会变成nil
  2. 在block中引用该弱引用,考虑到多线程情况,通过强引用strongSelf来引用该弱引用,如果self不为nil,就会retain self,以防在block内部使用过程中self被释放
  3. 强引用strongSelf在block作用域结束之后,自动释放

若在 Block 内如果需要访问 self 的方法、变量,建议使用 weakSelf;
若在 Block 内需要多次访问self,则需要使用 strongSelf。

七、视图与图像相关

1.UIView & CALayer的区别?
2.系统调用drawRect和layoutSubViews的时机?

注意:UIImageView是专门为显示图片做的控件,用了最优显示技术,是不让调用darwrect方法, 要调用这个方法,只能从uiview里重写。

drawRect调用时机:

  1. 当继承自UIView的视图被创建,并且设置frame后,并且已经添加到某个view,则会调用;若不设置frame则不会触发drawRect
  2. 当view某一属性例如背景色发生改变时,调用layoutIfNeeded,会触发drawRect;若view中布局或属性没有发生变化,则不会调用;

八、数据结构与算法

九、架构设计

十、系统基础知识

1.堆和栈区的区别?谁的占用内存空间大?

栈区:

  1. 由编译器自动分配并释放,不需要程序员管理变量的内存。一般用来存放函数的参数值,局部变量等
  2. 有静态分配和动态分配,都是有系统自动处理的
  3. 地址从高到低分配,遵循先进后出
  4. 只要栈的剩余空间大于stack 对象申请创建的空间,操作系统就会为程序提供这段内存空间,否则将报异常提示栈溢出

堆区:

  1. 由程序员手动管理内存的分配和释放
  2. 堆都是动态分配的,没有静态分配
  3. 地址从低到高分配,遵循先进先出

NSString 的对象就是stack 中的对象,NSMutableString 的对象就是heap 中的对象。前者创建时分配的内存长度固定且不可修改;
后者是分配内存长度是可变的,可有多个owner, 适用于计数管理内存管理模式。两类对象的创建方法也不同,前者直接创建“NSString * str1=@"welcome"; “,而后者需要先分配再初始化“ NSMutableString * mstr1=[[NSMutableString alloc] initWithString:@"welcome"]; ”
更多详细介绍

2.消息发送机制以及消息转发机制?

消息发送机制
Objective-C 是一门动态语言,我们大部分对象调用方法,例如[myClass someMethod]; runtime编译后都会变成objc_msgSend (myClass, @selector(someMethod))执行。
对象在runtim下的结构:

实例对象通过 isa 指针,找到类对象 Class;类对象同样通过 isa 指针,找到元类对象;元类对象也是通过 isa 指针,找到根元类对象;最后,根元类对象的 isa 指针,指向自己。可以发现 NSObject 是整个消息机制的核心,绝大数对象都继承自它。

  1. 先被编译成 ((void (*)(id, SEL))(void *) objc_msgSend)(myClass, @selector(printLog));
  2. 沿着入参 myClass 的 isa 指针,找到 myClass 的类对象(Class),也就是 MyClass
  3. 接着在 MyClass 的方法列表 method_lists 中,找到对应的 objc_method
  4. 最后找到 objc_method 中的 IMP 指针,执行具体的实现

消息转发机制
若上述方法列表 method_list 没找到对应的 selector 呢?系统会提供三次补救的机会:

+ (BOOL)resolveInstanceMethod:(SEL)sel {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
    if (sel == @selector(myTestPrint:)) {
#pragma clang diagnostic pop
        class_addMethod([self class],sel,(IMP)myMethod,"v@:@");
        return YES;
    } else {
        return [super resolveInstanceMethod:sel];
    }
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
    if (aSelector == @selector(myTestPrint:)) {
#pragma clang diagnostic pop
        return [Person new];
    } else {
        return [super forwardingTargetForSelector:aSelector];
    }
}
//生成一个有效的方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString: @"foo"]) {
        return [NSMethodSignature signatureWithObjCTypes: @"v@"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;
    Person *p = [Person new];
    ///这是一种更安全可靠的消息转发机制
    if ([p respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:p];
    } else {
        [self doesNotRecognizeSelector:sel];
    }
}
unrecognized selector sent to instance
3.进程和线程的区别?

进程是拥有资源的最小单位。线程是执行的最小单位,每个App启动就是一个进程,它可以包含多个线程工作。

4.TCP/IP介绍

十一、性能优化相关

1.内存泄漏检测原理

推荐使用 FBRetainCycleDetector
接入项目,在Debug模式下开启检测内存泄漏。

具体的方法是,为基类 NSObject 添加一个方法 -willDealloc 方法
该方法的作用是,先用一个弱指针指向 self,并在一小段时间(3秒)后,通过这个弱指针调用 -assertNotDealloc 而 -assertNotDealloc 主要作用是直接中断言。
- (BOOL)willDealloc {
    __weak id weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        //若3秒后,weakSelf依然没有被释放,则认为viewController没有被释放,发生内存泄漏,则抛出异常
        [weakSelf assertNotDealloc];
    });
    return YES;
}
- (void)assertNotDealloc {
     NSAssert(NO, @“异常”);
}
  1. 对于UIViewController,我们可以hook 掉 UIViewController 和 UINavigationController 的 pop 跟 dismiss 方法;
  2. 若UIViewController 被释放了,但它的 view 没被释放,或者一个 UIView 被释放了,但它的某个 subview 没被释放。这种内存泄露的情况很常见,因此,我们有必要遍历基于 UIViewController 的整棵 View-ViewController 树,采用递归遍历它的subView来找到未被释放的对象
  3. 其他对象若需要做特殊处理,需要加入白名单。
2.不要滥用宏定义

持续更新中...

上一篇下一篇

猜你喜欢

热点阅读