iOS-面试题2-Runtime、Runloop

目录:

  1. isa存储信息分析
  2. Class的内部结构、method_t、cache
  3. objc_msgSend底层调用流程
  4. super
  5. Runtime-API
  6. Runloop

一. Runtime

1. isa存储信息分析

  1. isa指针
    isa指针,在arm64架构之前,isa就是一个普通的指针,的确存储着类对象、元类对象的内存地址(实例对象的isa&ISA_MASK得到类对象的地址值,类对象的isa&ISA_MASK得到元类对象的地址值),从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息。

  2. 为什么使用共用体?

union isa_t
{
    Class cls;
    uintptr_t bits;
# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        //0,代表普通的指针,存储着Class、Meta-Class对象的内存地址
        //1,代表优化过,使用位域存储更多的信息
        uintptr_t nonpointer        : 1; 
        //是否有设置过关联对象,如果没有,释放时会更快
        uintptr_t has_assoc         : 1;
        //是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快
        uintptr_t has_cxx_dtor      : 1;
        //存储着Class、Meta-Class对象的内存地址信息
        uintptr_t shiftcls          : 33;
        //用于在调试时分辨对象是否未完成初始化
        uintptr_t magic             : 6;
        //是否有被弱引用指向过,如果没有,释放时会更快
        uintptr_t weakly_referenced : 1;
        //对象是否正在释放
        uintptr_t deallocating      : 1;
        //引用计数是否过大无法存储在isa中
        //如果为1,那么引用计数会存储在一个叫SideTable的结构体的refcnts成员中,refcnts是个散列表
        uintptr_t has_sidetable_rc  : 1;
        //里面存储的值是引用计数减1
        uintptr_t extra_rc          : 19;
    };
};

通过共用体将bits和结构体结合起来,而且自始至终一直都在操作bits,没有动结构体,结构体仅仅是为了可读性,所以不会影响bits里面的值,删除这个结构体也不影响。这种方式就是巧妙的利用共用体,达到了代码可读性的目的。

博客地址:Runtime1-isa存储信息分析

2. Class的内部结构、method_t、cache

  1. 关于method_t

method_t是对方法\函数的封装,源码如下:

struct method_t {
    SEL name;  //函数名(选择器)
    const char *types; //返回值类型、参数类型的编码
    IMP imp; //指向函数的指针(函数地址)
};

一个method_t就需要上面三个东西就够了,一个method_t就是一个方法。

  • 关于SEL name;
    ① SEL代表方法\函数名,一般叫做选择器,底层结构跟char *类似
    ② 可以通过@selector()和sel_registerName()获得
    ③ 可以通过NSStringFromSelector()和sel_getName()转成字符串
    ④ 不同类中相同名字的方法,所对应的方法选择器是相同的

  • const char *types;
    types包含了函数返回值类型、参数类型的编码。

  • IMP imp;
    IMP代表函数的具体实现,就是指向函数的地址

  1. 查找方法的过程

这里用对象方法解释,因为对象方法和类方法其实是一样的,就是放的位置不一样。

① 当某个对象调用某个方法,先根据isa找到当前类对象,在当前类对象的cache里面查找方法,如果查到就调用方法,查不到就去当前类对象的methods数组里面查找方法,如果查到就调用方法,并把方法缓存到cache里面。
② 如果当前类对象没查到,就根据superclass查找父类,同样先查找父类的cache,如果查到就调用方法,然后把父类cache里面的方法缓存到当前类对象的cache里面,如果父类的cache里面没查到,就去父类的methods数组里面查找方法,如果查到就调用这个方法并把父类的这个方法缓存到当前类对象的cache里面。
③ 如果父类也没有这个方法,再查找基类,同样先查找基类的cache再查找基类的methods数组,如果基类有这个方法就调用这个方法并把基类的这个方法放到当前类对象的cache缓存里面,这样下次再次调用这个方法就直接在当前类对象的cache里面取了,就不用遍历methods数组了。

博客地址:Runtime2-Class的内部结构、method_t、cache

3. objc_msgSend底层调用流程

  1. objc_msgSend的执行流程可以分为3大阶段
    ① 消息发送:就是根据isa、superclass寻找方法
    ② 动态方法解析:允许开发者动态创建新的方法
    ③ 消息转发:转发给另外一个对象调用这个方法

objc_msgSend内部的这三个阶段经历完还找不到方法就报错:unrecognized selector sent to instance/class。

消息发送.png
动态方法解析.png
消息转发.png
  1. 说一下消息转发流程
    当消息发送和动态方法解析都没找到方法就会进入消息转发阶段:
    ① 首先会调用+或-开头的forwardingTargetForSelector方法,如果这个方法返回值不为空,就给返回值发送SEL消息:objc_msgSend(返回值, SEL)。
    ② 如果这个方法的返回值为空,就会调用+或-开头的methodSignatureForSelector方法,如果这个方法返回值不为空,就会再调用+或-开头的forwardInvocation方法,我们可以在forwardInvocation里面方法做任何我们想做的事。
    ③ 如果这个方法的返回值为空,就会调用doesNotRecognizeSelector,报错unrecognized selector sent to instance/class。

  2. @synthesize自动生成_age成员变量、setter和getter的实现,@dynamic不自动生成_age成员变量、setter和getter的实现,正好是反过来的

博客地址:Runtime3-objc_msgSend底层调用流程

4. super

  1. [super message]的底层实现
    ① 消息接收者仍然是子类对象
    ② 从父类开始查找方法的实现

  2. 如何降低unrecognized selector sent to instance/class崩溃?说一下思路
    项目中我们可以给NSObject添加分类,实现forwardInvocation方法,在这里收集信息,然后上传到服务器。这里只是简单提个思路,其实NSProxy这个类是专门用来做消息转发的,以后再说。

博客地址:super

5. Runtime-API

交换方法实现在开发中经常使用,但是实际上我们使用最多的是交换系统或者第三方框架的方法。

  1. 如何拦截所有按钮的点击事件?

UIButton继承于UIControl,UIControl有一个sendAction:to:forEvent:方法,每当触发一个事件就会调用这个方法,所以我们可以给UIControl添加分类,在分类中交换这个方法的实现:

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // hook:钩子函数
        Method method1 = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
        Method method2 = class_getInstanceMethod(self, @selector(mj_sendAction:to:forEvent:));
        method_exchangeImplementations(method1, method2);
    });
}

- (void)mj_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
    NSLog(@"%@-%@-%@", self, target, NSStringFromSelector(action));

    // 调用系统原来的实现
    // 因为方法已经交换了,所以其实是调用sendAction:to:forEvent:
    [self mj_sendAction:action to:target forEvent:event];

    //拦截按钮事件
    if ([self isKindOfClass:[UIButton class]]) {
        // 拦截了所有按钮的事件

    }
}

上面交换方法也叫钩子函数,利用钩子函数就实现了拦截所有UIButton的点击事件。

  1. 为什么上面要加个dispatch_once?
    按理说load方法只会调用一次,万一别人主动调用了load方法那不就调用两次了吗,这样方法就交换两次了和没交换一样,所以加个dispatch_once。

  2. 交换方法实现的原理是什么?
    method_exchangeImplementations方法是传入两个Method,以前我们讲过Method的内部结构,其实交换方法实现就是把Method里面的IMP交换了。

  3. 对于交换方法,如果这个方法有缓存,怎么办?
    其实,调用method_exchangeImplementations函数会清空缓存,这样就保证了交换方法之后调用方法不会出错。

  4. 如何预防数组添加nil崩溃?
    我们可以交换insertObject:atIndex:方法,因为无论调用addObject:还是调用insertObject:atIndex:最后都会调用insertObject:atIndex:方法。给NSMutableArray添加分类,实现如下代码:

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 类簇:NSString、NSArray、NSDictionary,真实类型是其他类型
        Class cls = NSClassFromString(@"__NSArrayM");
        Method method1 = class_getInstanceMethod(cls, @selector(insertObject:atIndex:));
        Method method2 = class_getInstanceMethod(cls, @selector(mj_insertObject:atIndex:));
        method_exchangeImplementations(method1, method2);
    });
}

- (void)mj_insertObject:(id)anObject atIndex:(NSUInteger)index
{
    if (anObject == nil) return;
    
    [self mj_insertObject:anObject atIndex:index];
}
  1. 如何预防字典key传入nil崩溃?
    给NSMutableDictionary添加分类,交换setObject:forKeyedSubscript:方法,如下:
+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class cls = NSClassFromString(@"__NSDictionaryM");
        Method method1 = class_getInstanceMethod(cls, @selector(setObject:forKeyedSubscript:));
        Method method2 = class_getInstanceMethod(cls, @selector(mj_setObject:forKeyedSubscript:));
        method_exchangeImplementations(method1, method2);
        
        Class cls2 = NSClassFromString(@"__NSDictionaryI");
        Method method3 = class_getInstanceMethod(cls2, @selector(objectForKeyedSubscript:));
        Method method4 = class_getInstanceMethod(cls2, @selector(mj_objectForKeyedSubscript:));
        method_exchangeImplementations(method3, method4);
    });
}

- (void)mj_setObject:(id)obj forKeyedSubscript:(id)key
{
    if (!key) return;
    
    [self mj_setObject:obj forKeyedSubscript:key];
}

- (id)mj_objectForKeyedSubscript:(id)key
{
    if (!key) return nil;
    
    return [self mj_objectForKeyedSubscript:key];
}
  1. 什么是Runtime?平时项目中有用过么?
    ① OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行。
    ② OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数。
    ③ 平时编写的OC代码,底层都是转换成了RuntimeAPI进行调用。

  2. Runtime具体应用在哪里?
    ① 利用关联对象(AssociatedObject)给分类添加属性
    ② 遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)
    ③ 交换方法实现(交换系统的方法)
    ④ 利用消息转发机制解决方法找不到的异常问题
    ......

博客地址:Runtime-API

二. Runloop

  1. 什么是Runloop?
    顾名思义,Runloop就是运行循环,就是在程序运行过程中循环做一些事情。

  2. RunLoop的基本作用
    ① 保持程序的持续运行
    ② 处理App中的各种事件(比如触摸事件、定时器事件等)
    ③ 节省CPU资源,提高程序性能:该做事时做事,该休息时休息

  3. 两套RunLoop API
    ① NSRunLoop和CFRunLoopRef都代表着RunLoop对象
    ② NSRunLoop是基于CFRunLoopRef的一层OC包装
    ③ CFRunLoopRef是开源的:Core Foundation源码

  4. RunLoop与线程的关系
    ① 每条线程都有唯一的一个与之对应的RunLoop对象,主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop,RunLoop会在线程结束时销毁。
    ② RunLoop是懒加载的,线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建。
    ③ 主线程几乎所有的事情都是交给了runloop去做,比如UI界面的刷新、点击时间的处理、performSelector等等
    ④ RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value。

  5. Core Foundation中关于RunLoop的5个类的关系

CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

CFRunLoopRef是指向__CFRunLoop结构体的指针,找到__CFRunLoop结构体源码:

struct __CFRunLoop {
    ......
    pthread_t _pthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;//当前模式
    CFMutableSetRef _modes; //是个集合,无序的,里面装的是CFRunLoopModeRef类型的对象
    ......
};

__CFRunLoop里面有个_modes,它是个集合,里面装的是一堆CFRunLoopModeRef类型的对象,当前的模式是_currentMode。

进入CFRunLoopModeRef:

typedef struct __CFRunLoopMode *CFRunLoopModeRef;

struct __CFRunLoopMode {
    ......
    CFStringRef _name;//名称
    CFMutableSetRef _sources0;//里面装的是CFRunLoopSourceRef类型的对象
    CFMutableSetRef _sources1;//里面装的是CFRunLoopSourceRef类型的对象
    CFMutableArrayRef _observers;//里面装的是CFRunLoopObserverRef类型的对象
    CFMutableArrayRef _timers;//里面装的是CFRunLoopTimerRef类型的对象
    ......
};

CFRunLoopRef里面有个_modes集合,里面装好多CFRunLoopModeRef类型的模式,_currentMode是当前模式。

模式里面有name,_sources0、_sources1集合(里面装的是CFRunLoopSourceRef类型的东西),_observers数组(里面装的是CFRunLoopObserverRef类型的东西),_timers数组(里面装的是CFRunLoopTimerRef类型的东西)。

如下图所示:

结构.png

① CFRunLoopModeRef代表RunLoop的运行模式。
② 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer。
③ RunLoop启动时只能选择其中一个Mode,作为currentMode,如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入。
④ 不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响。
⑤ 如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出。
⑥ 其中Timer是定时器,平时创建的一些定时器都放在这里,Observer是监听器,source0/Source1是事件,比如点击事件、performSelector等等。

  1. 为什么多种模式要分开呢?
    比如scrollView滚动的时候让它切换到滚动模式,那么在滚动模式下,scrollView就专心处理滚动相关的就可以了,以前模式下的事情就不处理了。如果不滚动,在正常模式下,就专心处理正常模式下的事情就好了,这样可以做到流畅不卡顿。

  2. 常见的两种Mode
    ① kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行。
    ② UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。

  3. Source0、Source1、Timers、Observers分别代表什么呢?
    Source0:触摸事件处理、performSelector:onThread:。
    Source1:基于Port(端口)的线程间通信、系统事件捕捉 (比如点击事件,通过Source1捕捉,然后包装成Source0进行处理)。
    Timers:NSTimer、performSelector:withObject:afterDelay:(底层就是NSTimer)。
    Observers:用于监听RunLoop的状态、UI刷新(BeforeWaiting)、Autorelease pool(BeforeWaiting)。

比如,点击界面空白就是Source0事件。关于Observers监听UI刷新(BeforeWaiting),self.view.backgroundColor = [UIColor redColor];这句代码并不是立马执行,Observers会先记下来,当Observers监听到RunLoop将要睡觉啦,就在RunLoop将要睡觉之前执行(刷新UI)。同理Autorelease pool也是一样,当Observers监听到RunLoop将要睡觉啦,就在RunLoop睡觉之前释放对象。

  1. RunLoop有几种状态?
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),          //即将进入RunLoop
    kCFRunLoopBeforeTimers = (1UL << 1),   //即将处理Timer
    kCFRunLoopBeforeSources = (1UL << 2),  //即将处理Source
    kCFRunLoopBeforeWaiting = (1UL << 5),  //即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),   //即将从休眠中唤醒
    kCFRunLoopExit = (1UL << 7),           //即将退出RunLoop
    kCFRunLoopAllActivities = 0x0FFFFFFFU  //所有状态
};
  1. 如何监听RunLoop的状态?
    Observers数组里面有系统创建的一些Observer,用于监听RunLoop状态进行UI刷新、Autorelease pool等,如果我们自己想监听RunLoop状态肯定要自己创建Observer。
    首先使用CFRunLoopObserverCreate创建observer,然后再用CFRunLoopAddObserver添加Observer到RunLoop中。

  2. RunLoop运行流程图

RunLoop的运行流程.png
  1. 下面代码是谁处理?

一般情况下GCD的东西是GCD来处理的,不会交给RunLoop。GCD是GCD,RunLoop是RunLoop,他们互不干扰,但是有一种情况下GCD是交给RunLoop处理的,如下:

- (void)viewDidLoad {
    [super viewDidLoad];

    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        // 子线程处理一些逻辑

        // 回到主线程去刷新UI界面
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"11111111111"); // 断点
        });
    });
}

当我们在子线程处理一些逻辑然后回到主线程去刷新UI界面,这种情况就会交给RunLoop去处理GCD相关的东西,然后再回到GCD。

  1. RunLoop线程休眠的实现原理?
    线程休眠就是因为用户态和内核态的切换。

  2. RunLoop在实际开发中的应用有哪些?
    ① 解决NSTimer在滑动时停止工作的问题
    ② 控制线程生命周期(线程保活)
    ③ 监控应用卡顿
    ④ 性能优化

  3. NSRunLoopCommonModes是什么?
    NSRunLoopCommonModes并不是一个真的模式,它只是一个标记,定时器能在_commonModes数组中存放的模式下工作。

  4. timer 与 runloop 的关系?
    ① RunLoop对象里面有个_modes数组,里面放一堆模式,模式里面会放timer,如果timer被标记为commonModes,那么timer就能在_commonModes数组中存放的模式下工作,能在commonModes“模式”下工作的东西都会被添加到_commonModeItems数组里中。
    ② 如果线程休眠了,timer也可以唤醒休眠的RunLoop。

  5. runloop 是怎么响应用户操作的,具体流程是什么样的?
    当用户有个点击事件,这个系统事件会先被Source1捕捉,Source1捕捉之后会包装成事件队列(EventQuene),再放到Source0里面进行处理,然后RunLoop循环再处理Source0里面的事件。

博客地址:
Runloop
线程保活

你可能感兴趣的:(iOS-面试题2-Runtime、Runloop)