Loading...
墨滴

weak_PG

2021/09/30  阅读:34  主题:默认主题

这些面试“iOS常问底层”知识点,看看都会了吗?

写在前面

哈喽,安纳达。该篇文章3000字左右,阅读时长大概20分钟左右呦,内容都是整合了最近面试常用的问题,具体的更细的细节,后面会持续更新。

[self class][super class]分别指什么?

// LGTeacher继承于LGPerson,这里输出什么
- (instancetype)init{
    self = [super init];
    if (self) {
        NSLog(@"%@ - %@",[self class],[super class]);// 打印LGTeacher,LGTeacher
    }
    return self;
}

self是个形参名,而super是个关键字,这里的[self class]其实调用的是NSObject的class方法。就是发送消息objc_msgSend(void/* id self, SEL op, ... */ ),消息接受者是 self也就是LGTeacher;super其实是个关键字objc_msgSendSuper(void/* struct objc_super *super, SEL op, ... */ )是个struct objc_super的结构体.[super class] 本质就是objc_msgSendSuper注意:这里有一行注释,获取当前的类而不是它的父类,也就是LGTeacher

block引用问题

下面这些控制台输出什么

NSObject *objc = [NSObject new];
NSLog(@"%ld",CFGetRetainCount(__bridge CFTypeRef)(objc))); //1

void(block1)(void) = ^{
 NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef(objc)));//3
}
block1();

void(^ __weak block2)(void)= ^{
    NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));// 4
}
block2();

void(block3)(void) = [block2 copy]; //5
block3();

__block NSObject *obj= [NSObject new]; 
void(^block4)(void)= ^{
    NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(obj))); //1
}
block4();
  • 第一个打印为1毫无疑问
  • 第二个普通的block捕获外部变量,对象属性的拷贝作为自己的成员变量,然后再copy一份。汇编__block_copy时候是2,走到NSMallocBlock的时候就变成了3
  • 第三个__weak 修饰的是个栈block, 只做了+1的操作
  • 第四个 copy + 1 234验证了strongBlock = __weak weakBlock + [weakBlock copy]

GCD实现读写锁

读写锁具有以下特点:

  • 同一时间,只能有一个线程进行写的操作
  • 同一时间,允许多个线程进行读的操作
  • 同一时间,不允许即有写的操作又有读的操作
// 单写
- (void)safeSetter: (NSString *)name time:(int)time {
    dispatch_barrier_async(self.safeQu, ^{
        sleep(time);
        [self.dict setValue:name forKey:@"test"];
        NSLog(@"写入 %@ - %@",self.dict[@"test"], [NSThread currentThread]);
    });
}
// 多读
- (NSString *)safeGetter {
    __block NSString * result;
    // 加个同步 保证在当前线程读取是正确的 不然result返回的是null
    dispatch_sync(self.safeQu, ^{
        result = self.dict[@"test"];
    });
    return result;
}

dispatch_barrier_async栅栏函数,在这之后的异步任务需要等到dispatch_barrier_async执行完后才可以执行,通过dispatch_sync来阻塞保证任务同步读取的是正确的

@synchronized为什么应用频次最多

  • @synchronized底层是封装了一把递归锁,可以自动进行加锁解锁
  • @synchronized中的lockCount控制递归,而threadCount控制多线程,解决了锁的可重入递归性
  • @synchronized锁的写法也是最简单的

block类型,怎么区分?

  • GlobalBlock:位于全局区,在block内部不使用外部变量,或者只使用静态变量或者全局变量
  • MallocBlock:位于堆区,在block内部使用局部变量或者OC属性,并且赋值给强引用或者copy修饰的变量
  • StackBlock:位于栈区,与堆区的block一样,可以在内部使用局部变量或者OC属性,但是不能赋值给强引用或者copy修饰的变量

KVC普通对象的setter过程

  • 比如本文中的name,顺序就是setName->_setName->setIsName。当找到这三种setter中任意一个时,则进行赋值
  • 判断+ (BOOL)accessInstanceVariablesDirectly函数是否返回YES,如果返回YES,则按照 _key->_iskey->key->iskey的顺序搜索成员,找到任意一个则进行赋值。如果返回NO,系统会执行该对象的setValue:forUndefinedKey:函数,默认抛出异常

KVO底层原理机制分析

KVO是基于runtime机制实现的,KVO运用了isa-swizzling技术,就是类型混合指针机制,将2个对象的isa指针互相调换,就是俗称的黑魔法。

当某一个的属性对象第一次被观察时,系统就会在运行期动态的创建该类的派生类NSKVONotifying_xxx,在这个派生类中重写基类中的任何被观察属性的setter方法,派生类在重写的setter方法中实现真正的通知机制。

每个类对象中都有一个isa指针指向当前的类,当一个类对象被观察的时候,那么系统会偷偷的将isa指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是派生类的setter方法

键值观察通知依赖于NSObject的两个方法,willChangeValueForKey:didChangeValueForKey,在一个被观察属性被改变之前,willChangeValueForKey:一定会被调用,这就会记录旧的值。而当发生改变之后didChangeValueForKey会被调用,继而通过消息或者响应机制去调用observerValueForKey:ofObject:change:context:removeObserver的时候isa再指回来。

参数入栈和结构体入栈顺序

下面栈帧入栈的情况?

- (void)viewDidLoad {// 参数压栈(id self, sel _cmd)
    [super viewDidLoad];// 结构体压栈(objc, class) 
    Class cls = [Person class];
    void  *kc = &cls; // kc并不会入栈 是类的指针地址
    [(__bridge id)kc saySomething];
    Person *person = [Person alloc];
}

self对象 -> cmd(viewDidLoad) -> self类入栈 -> self 对象 -> Person类入栈 -> Person对象

iOS线程如何保活,为什么要线程保活

一般情况下开启线程任务之后,当任务执行完毕线程就会被销毁,如果想让线程不死的话,需要为线程添加一个runloop,在实际的开发中经常会遇到一些耗时且需要频繁处理的工作,这部分工作与UI无关,比如说大文件的下载,后台间隔一段时间进行数据的上报,APM开启一个watch dog线程等。

NSRunLoop *runloop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
[runLoop run];

保活的线程如何回收?

// 如下方式创建的线程,self会引用thread,thread会引用self,早成循环引用
KCThread *thread = [[KCThreadalloc] initWithTarget: self selector: @selector(sayNB) object:nil];
- (void)exitThread {
 // 需要在回收的时候如下设置
 self.stopped=YES;
 // 停止runloop
 CFRunLoopStop(CFRunLoopGetCurrent());
 [self.thread cancel];
 // 解决循环引用问题
 self.thread=nil;
 NSLog(@"%s%@",__func__,[NSThreadcurrentThread]);
}

循环引用,为什么要在block中添加strong

通常我们在解决循环引用的时候是利用weak typeof(self) weaksel self 虽然通过弱引用的 weakSelf解决循环引用的无 法释放的问题但是会存在释放过早的问题!

例如在block内部加入延时,但是用户操作过快导致self提前释放!我们通过__strong __typeof(weakSelf) strongSelf = weakSelf;延长了生命周期这样在strongSelf在作用空间能够有效并且出了作用域也能及时的回收,因为strongSelf是临时变量

dispatch_once底层

void
dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func)
{
 dispatch_once_gate_t l = (dispatch_once_gate_t)val;

#if !DISPATCH_ONCE_INLINE_FASTPATH || DISPATCH_ONCE_USE_QUIESCENT_COUNTER
 uintptr_t v = os_atomic_load(&l->dgo_once, acquire);
 if (likely(v == DLOCK_ONCE_DONE)) {
  return;
 }
#if DISPATCH_ONCE_USE_QUIESCENT_COUNTER
 if (likely(DISPATCH_ONCE_IS_GEN(v))) {
  return _dispatch_once_mark_done_if_quiesced(l, v);
 }
#endif
#endif
 if (_dispatch_once_gate_tryenter(l)) {
  return _dispatch_once_callout(l, ctxt, func);
 }
 return _dispatch_once_wait(l);
}

进入dispatch_once_f源码,其中的val是外界传入的onceToken静态变量,而funcdispatch_Block_invoke(block),其中单例的底层主要分为以下几步

  • 将val,也就是静态变量转换为dispatch_once_gate_t类型的变量
  • 通过os_atomic_load获取此时的任务的标识符v
  • 如果v等于DLOCK_ONCE_DONE表示任务已经执行过了,直接return
  • 如果任务执行后,加锁失败了,则走到dispatch_once_mark_done_if_quiesced函数,再次进行存储,将标识符置为 DLOCK_ONCE_DONE
  • 反之,则通过_dispatch_once_gate_tryenter 尝试进入任务即解锁,然后执行dispatch_once_callout执行 block回调
  • 如果此时有任务正在执行再次进来一个任务2则通过dispatch_once_wait函数让任务2进入无限次等待

iOS多线程原理和线程生命周期是什么样

  • 对于单核CPU,同一时间,CPU只能处理一条线程,即只有一条线程在工作,iOS中的多线程同时执行的本质是CPU在多个任务直接进行快速的切换,由于CPU调度线程的时间足够快就造成了多线程的“同时”执行的效果。其中切换的时间间隔就是时间片。多核cpu具有真正意义上的并发!
  1. 新建状态:用new关键字建立一个线程后,该线程对象就处于新建状态。处于新生状态的线程有自己的内存空间,通过调用start()方法进入就绪状态。
  2. 就绪状态:处于就绪状态线程具备了运行条件,但还没分配到CPU,处于线程就绪队列,等待系统为其分CPU。 当系统选定一个等待执行的线程后,它就会从就绪状态进入运行状态,该动作称为“CPU调度”。
  3. 运行状态:在运行状态的线程执行自己的run方法中代码直到等待某资源而阻塞或完成任务而死亡。如果在给定的时间片内没有执行结束,就会被系统给换下来回到就绪状态。
  4. 阻塞状态:处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己运行,进入阻塞状态。在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待被系统选中后从原来停止的位置开始继续执行。
  5. 死亡状态:死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有三个,一个是正常运行的线程完成了它的全部工作;另一个是线程被强制性地终止,如通过exit方法来终止一个线程【不推荐使用】;三是线程抛出未捕获的异常。

简述信号量和调度组的原理

信号量作用一般是用来使任务同步执行,类似于互斥锁,用户可以根据需要控制GCD最大并发数! dispatch_semaphore_create 创建信号量,里面的数字是表示最大并发数 dispatch_semaphore_wait 信号量等待 -1 dispatch_semaphore_signal信号量释放 +1

  1. 创建信号量,并控制通行的额值dispatch_semaphore_t sem = dispatch_semaphore_create(1)
  2. dispatch_semaphore_wait底层源码中是一个原有信号量-1的一个操作,当我们信号量值小于0的时候会进入判断等待的时间.如果设定为DISPATCH_TIME_FOREVER就进入do-while死循环等待信号发起
  3. dispatch_semaphore_signal原理
  • 里面os_atomic_inc2o原子操作自增加1,然后会判断,如果value>0,就会返回0。
  • 例如value+1之后还是小于0,说明是一个负数,也就是调用disatch_semaphore_wait次数太多了,加一次后依然小于0,并且==LONG_MIN就报异常Unbalanced call to dispatch_semaphore_signal()
  • 然后会调用_dispatch_semaphore_signal_slow方法做容错的处理,_dispatch_semaphore_signa是一个do-while 循环调用semaphore_siqnal这样就能响应dispatch_semaphore_wait陷入的死循环等待

调度组最直接的作⽤:控制任务执⾏顺序

  • dispatch_group_create :创建组
  • dispatch_group_async: 进组任务
  • dispatch_group_notify : 进组任务执⾏完毕通知
  • dispatch_group_wait : 进组任务执⾏等待时间
  • dispatch_group_enter: 进组
  • dispatch_group_leave :出组
  1. dispatch_group_create创建组控制所有调度组的状态
  2. 进组和出组
  • dispatch_group_enter:调度组value-1.等待信号
  • dispatch_group_enterold_value == DISPATCH_GROUP_VALUE_MAX证明

Too many nested calls to dispatch_group_enter()也会报错

  • dispatch_group_leave:调度组value+1,达到最开始设定的值就会调用 dispatch_group_wake去唤醒 dispatch_group_enter堵塞的状态,进而去执行dispatch_continuation_async执行任务
  • dispatch_group_leave +1还是 value=0 证明进组和出组不匹配
  • dispatch_group_async进组任务封装了 dispatch_group_enter进组 + dispatch_group_leave出组
  • dispatch_group_notify:当old_state=0的时候调用_dispatch_group_wake 也就是调用 block的callout,与leave调用了同一个方法其实起到一个监听通知执行的效果

__block修饰变量被block捕获之后的情况

  • 第一层拷贝对象本身从栈区copy到堆区.因为被 block修饰所以中间会产生Blockbyref这样的结构体其中带有相关信息的成员变量
  • 第二层:因为被block修饰所以中间会产生Block_byref这样的结构体其中带有相关信息的成员变量!
  • 第三层:外界Block_byref会被Block捕获之后,会赋值(copy)一份!通过Block_object_assign函数调用*dest=_Block_byref_copy(object);发起对Block_byref的拷贝
struct Block_byref {
 void *__ptrauth_objc_isa_pointer isa: 
    struct Block_byref *forwarding;
 volatile int32_t flags;// contains ref count 
    uint32_t size;
};

struct Block_byref_2 {
 // requires BLOCK_BYREF_HAS_COPY_DISPOSE
 BlockByrefKeepFunction byref_keep; //=__Block_byref_id_object_copy_131
 BlockByrefDestroyFunction byref_destroy; // =___Block_byref_id_object_dispose_131
}

struct Block byref 3 {
 // requires BLOCK_BYREF_LAYOUT_EXTENDED 
    const char *layout;
}

其中在真正执行的的时候就会调用:BlockByrefKeepFunction byref_keep函数进行object_copy进而调用: Block_object_assign对外界对象的拷贝过程!

weak_PG

2021/09/30  阅读:34  主题:默认主题

作者介绍

weak_PG