网络知识 娱乐 2022年iOS最新面试(底层基础)问题答案

2022年iOS最新面试(底层基础)问题答案

每条题目都是自己做的,请点赞三连

文章目录

      • Runloop
      • 线程、队列、锁
      • GCD
      • KVC、KVO
      • ISA、类结构
      • 消息转发
      • 引用计数、weak、autoreleasepool
      • 内存检测、OOM
      • 分类、扩展、关联对象
      • NSMutableArray扩展
      • Timer、锁
      • TCP/IP协议族
      • 二叉树、排序
      • 性能优化
      • 算法

Runloop

1、RunLoop 的本质是什么?

答:本质是一个OC对象,内部也有isa指针。

2、Runloop和线程是什么关系?

答:线程和 RunLoop 之间是Key-value的对应关系,是保存在一个全局的 Dictionary 里,线程是key,RunLoop是value,而且是懒加载的。

3、Runloop的底层数据结构是什么样的?有几种 运行模式(mode)?每个运行模式下面的 CFRunloopMode 是哪些?他们分别是什么职责?

答:Runloop的底层数据结构(NSRunLoop是CFRunLoop的封装):

CFRunLoop,RunLoop对象
Mode,运行模式
Source,输入源/事件源
Timer,定时源
Observer,观察者

系统默认注册了5个Mode常用的有3个 常见的几种Mode:

Default : App的默认Mode,通常主线程是在这个Mode下运行
UITracking: 界面跟踪Mode,用于ScrollView`追踪触摸滑动,保证界面滑动时不受其他Mode影响。

Common :并不是一个真的模式,它只是一个标记,如:被标记的 Timer可以在Default模式和UITracking下运行。

基本用不到的Mode:

UIInitialization :私有的mode,App启动的时候的状态,加载出第一个页面后,就转成了Default
GSEventReceive系统的内部 Mode,通常用不到

4、Runloop 的监听状态有哪几种?

答:Entry->BeforeTimers->BeforeSources->BeforeWaiting(休眠)->AfterWaiting(唤醒)->Exit->AllActivities

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),                 // 即将进入Loop
    kCFRunLoopBeforeTimers = (1UL << 1),          // 即将处理Timer
    kCFRunLoopBeforeSources = (1UL << 2),         // 即将处理Source
    kCFRunLoopBeforeWaiting = (1UL << 5),         // 即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),          // 刚从休眠中唤醒
    kCFRunLoopExit = (1UL << 7),                  // 即将退出Loop
    kCFRunLoopAllActivities = 0x0FFFFFFFU         // 所有状态
};

5、Runloop 的工作流程大概是什么样的?
在这里插入图片描述

6、Runloop 有哪些应用?

答:滑动scrollview时候的mode切换,cell的图片下载 将多个耗时操作分开执行,在每次 RunLoop
唤醒时去做一个耗时任务。

7、Runloop的内核态和用户态?

答:

  • CPU的两种工作状态:内核态和用户态(或者称管态和目态)

  • 内核态:系统中既有操作系统的程序,也由普通用户的程序。为了安全和稳定性操作系统的程序不能随便访问,这就是内核态,内核态可以使用所有的硬件资源。

  • 用户态:不能直接使用系统资源,也不能改变CPU的工作状态,并且只能访问这个用户程序自己的存储空间。

线程、队列、锁

  1. 线程、队列的关系? 一个线程是否可能存在于两个队列?

答:线程是系统调度的最小任务单位,队列是存放管理任务单位的数据结构。

  1. 队列一定会创建线程吗?

答:不,同步执行方式是不创建新线程的,就在当前线程嗨。

线程按执行方式分为同步、异步,按队列管理分为串行并行,这样有四种组合,加上常说的主线程主队列,那么结合执行方式就有六种组合。

同步串行,不创建线程,所以还是在当前线程一个一个做

同步并行,不创建线程,所以就算是并行,也还是在当前线程一个一个做

异步串行,开辟多一条线程,任务在新开辟的一条线程里面一个一个做

异步并行,开辟多条线程,任务在新开辟的线程里面一起做

同步主队,阻塞

异步主队,同异步串行,因为主队就是串行,但是不开辟新线程,因为主线程是全局的单例的

  1. 队列是否可以无限制创建?

答:不能,队列也是对象,要占用内存,受限于硬件资源,不能无限制创建。

  1. PerformSelector & NSInvocation优劣对比

答:

相同点: 有相同的父类NSObject 区别: 在参数个数<=
2的时候performSelector:的使用要简单一些,但是在参数个数 > 2的时候NSInvocation就简单一些。

  1. gcd 的使用,能不能取消?

答:dispatch_block_cancel可以取消尚未执行的任务。已经在运行的,用代码中断

  1. 如何进行线程保活

答:想让线程不死掉的话,需要为线程添加一个RunLoop

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 往RunLoop里面添加SourceTimerObserver,Port相关的是Source1事件
// 添加了一个Source1,但是这个Source1也没啥事,所以线程在这里就休眠了,不会往下走,----end----一直不会打印
[runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
[runLoop run];
  1. 编程题 3个线程顺序打印 0-100

答:如下

- (void)print0_100 {
    
    __block int i = 0;
    dispatch_queue_t queueA = dispatch_queue_create("queue a", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queueB = dispatch_queue_create("queue b", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queueC = dispatch_queue_create("queue c", DISPATCH_QUEUE_CONCURRENT);

    NSCondition *condition = [[NSCondition alloc] init];
    
    dispatch_async(queueA, ^{
        while (1) {
            [condition lock];
            while (i%3 != 0) {
                [condition wait];
            }
            if (i > 100) {
                [condition unlock];
                return;
            }
            NSLog(@"A ==== i = %d",i);
            i++;
            [condition broadcast];
            [condition unlock];
        }
    });
    
    dispatch_async(queueB, ^{
        while (1) {
            [condition lock];
            while (i%3 != 1) {
                [condition wait];
            }
            if (i > 100) {
                [condition unlock];
                return;
            }
            NSLog(@"B ==== i = %d",i);
            i++;
            [condition broadcast];
            [condition unlock];
        }
    });

    dispatch_async(queueC, ^{
        while (1) {
            [condition lock];
            while (i%3 != 2) {
                [condition wait];
            }
            if (i > 100) {
                [condition unlock];
                return;
            }
            NSLog(@"C ==== i = %d",i);
            i++;
            [condition broadcast];
            [condition unlock];
        }
    });

}

GCD

  1. GCD、NSOperation区别, 功能方法区别.

答:

  • NSThread是早期的多线程解决方案,实际上是把C语言的PThread线程管理代码封装成OC代码。
  • GCD是取代NSThread的多线程技术,C语法+block。功能强大。
    • 充分利用多核,效率最高
  • NSOperationQueue是把GCD封装为OC语法,额外比GCD增加了几项新功能。
    • 最大线程并发数
    • 取消队列中的任务
    • 暂停队列中的任务
    • 可以调整队列中的任务执行顺序,通过优先级
    • 线程依赖
    • NSOperationQueue支持KVO。 这就意味着你可以观察任务的状态属性。
      但是NSOperationQueue的执行效率没有GCD高,所以一半情况下,我们使用GCD来完成多线程操作。
  1. gcd queue 的区别

答:

  • GCD是取代NSThread的多线程技术,C语法+block。功能强大。

    • 充分利用多核,效率最高
  • NSOperationQueue是把GCD封装为OC语法,额外比GCD增加了几项新功能。

    • 最大线程并发数

    • 取消队列中的任务

    • 暂停队列中的任务

    • 可以调整队列中的任务执行顺序,通过优先级

    • 线程依赖

    • NSOperationQueue支持KVO。 这就意味着你可以观察任务的状态属性。
      但是NSOperationQueue的执行效率没有GCD高,所以一半情况下,我们使用GCD来完成多线程操作。

  1. group 如何实现barrier类似的功能?

答:barrier栅栏功能,栅栏前不管多少个异步都要执行完毕,才会执行栅栏后面的操作。

可以尝试用信号量来实现,例如A、B、C、barrier、D并发,但是希望ABC完成后D才开始。

设定线程信号量最大值为3,ABC先执行,等ABC都执行完,D才开始

  1. GCD group 如何实现同步的? (还能用什么实现?)

答:第一串行队列,第二并行队列,第三分组,第四信号量。

串行,一个一个执行,有同步操作的效果

并行,先开一个异步线程,把多个同步线程放在该异步中执行并行

分组,dispatch_group_notify()提供了一个知道group什么时候结束的点

信号量,信号量和锁的作用差不多,可以用来实现同步方式

  1. 执行一个 NSThread 任务, 如何在执行过程中让他终止?

答://监测当前线程是否被取消过,如果被取消了,则该线程退出。在线程里面检测取消的标记,然后执行退出
if ([[NSThread currentThread] isCancelled])
{
[NSThread exit];
}

  1. iOS NSOperation 是如何终止/取消任务的?

答:正在执行的任务,NSOperation也是不能取消的,可以考虑用一个条件来做,满足条件则执行此任务,不满足则不执行

  1. 多线程,异步执行(async)一个performSelector 会执行么?如果加上 afterDelay呢?

答:performSelector会执行,afterDelay不会执行;原因performSelector只是单纯的直接调用某函数,afterDelay是在该子线程执行一个NSTimer,注意一点:子线程中的runloop默认是没有启动的状态,要想afterDelay生效,要runloop在线程有事务的状态下跑起来,所以需要执行[[NSRunLoop
currentRunLoop] run]。

  1. GCD 实现 NSOperationQueue

答:莫名其妙的题目,要阐述怎么用GCD缔造出NSOperationQueue吗

  1. DispatchQoS的作用

答:线程优化,告诉系统是什么类型的任务。

user_interactive:用户交互(希望尽快完成,用户对结果很期望,不要放太耗时操作)

user_initiated:用户期望(不要放太耗时操作)

default:默认(不是给程序员使用的,用来重置对列使用的)

utility:实用工具(耗时操作,可以使用这个选项)

background:后台

unspecified:未指定

iOS 7.0 之前 优先级

priority_high:高优先级

priority_default:默认优先级

priority_low:低优先级

priority_backgroud:后台优先级

KVC、KVO

  1. 结构体的字节对齐和OC对象的字节对齐?

答:
在这里插入图片描述
各种数据类型所占的内存大小:

1byte: bool、char、int8_t

2byte: short、int16_t、unichar

4byte: int32_t、int、NSIneteger(32)、float、CGFloat(32)

8byte: longlong、int64_t、double、CGFloat(64)

唯一的差异:

long、unsigned
long、NSIneteger(64位)、NSUIneteger(64位),32位系统是4byte,64位系统是8byte

例子:char=1byte,int=4byte

typedef struct ademo {
char a,char b,char c,char d,
}ademo

上面占4字节

struct HFStruct1 {
    char a;      //1{所占字节数}
    double b;   // 8{所占字节数}
    int c;      // 4{所占字节数}
    short d;    // 2{所占字节数}
}struct1;

本来应该是1+8+4+2=15byte

a
bbbbbbbb
ccccdd

最终长度:1+7(碎片)+8+4+2+2(碎片)=24byte

struct HFStruct2 {
    double a;      //8{所占字节数} 
    int b;     //4{所占字节数}
    char c;    //1{所占字节数} 
    short d;    //2{所占字节数}
}struct2;
aaaaaaaa
bbbbcdd

最终长度:8+4+1+2+1(碎片)=16byte

  1. instance(实例对象)、class(类对象)、meta-class(元类对象)分别储存了什么信息?为什么要设计元类?

答:

instance对象在内存中存储的信息包括
(1)isa指针 (2)其它成员变量

class对象在内存中存储的信息主要包括:
(1)isa指针
(2)superclass指针
(3)类的属性信息(@property)、类的对象方法信息(instance method)
(4)类的协议信息(protocol)、类的成员变量信息(ivar)

meta-class对象和class对象的内存结构是一样的,但是用途不一样,在内存中存储的信息主要包括
(1)isa指针
(2)superclass指针
(3)类的类方法信息(class method)

为什么存在元类的设计?从面向对象的设计理念来说,万物皆对象,类也是对象,描述类的元类,存在的目的就是自上而下的逻辑自洽,也方便message的传递。

  1. KVO的具体实现流程?访问成员变量(类似self->age)会触发KVO嘛?KVC会触发KVO嘛?KVO的两个核心调用方法是?

答:实现的流程

a. 添加属性的KVO 会触发Runtime 创建一个NSKVONotifying_XXX的内部隐藏类。
b.NSKVONotifying_XXX的set 方法会调用Foundation的_NSSetObjectValueAndNotify方法。
c. _NSSetObjectValueAndNotify内部会调用。

willChangeValueForKey
[ super setName:]
didChangeValueForKey

d. didChangeValueForKey会发消息给KVOobserveValueForKeyPath方法

访问成员变量不会触发KVO,因为KVO是建立在类似setter和getter的机制之上。

KVC会触发。

属性发生改变之前调用willChangeValueForKey:方法,在发生改变之后调用didChangeValueForKey:方法,必须按顺序调用,否则失效

  1. KVC的原理?getter 和 setter 的搜索策略是什么?KVC 有什么实际的应用?

答:

在这里插入图片描述

ISA、类结构

  1. isa 指针是什么?里面有哪些特殊的位数?什么是TaggedPointer的优化?

答:

对象.isa -> 类.super -> 父类.super -> 根类.super -> nil

类.isa -> 元类.super -> 父元类.super -> 根元类.super -> 根类.super -> nil

元类.isa = 父元类.isa = 根元类.isa = 根元类

在arm64架构之前,isa就是一个普通指针,存储着Class、Meta-Class对象的内存地址;从arm64架构开始,变成了一个共用体union结构,还使用位域来存储更多的信息。

//共用体中可以定义多个成员,共用体的大小由最大的成员大小决定
//共用体的成员公用一个内存
//对某一个成员赋值,会覆盖其他成员的值
//存储效率更高
union isa_t 
{
    Class cls;
    uintptr_t bits;   //存储下面结构体每一位的值
    struct {
        uintptr_t nonpointer        : 1;  // 0:普通指针,存储Class、Meta-Class;1:存储更多信息
        uintptr_t has_assoc         : 1;  // 有没有关联对象
        uintptr_t has_cxx_dtor      : 1;  // 有没有C++的析构函数(.cxx_destruct)
        uintptr_t shiftcls          : 33; // 存储Class、Meta-Class的内存地址
        uintptr_t magic             : 6;  // 调试时分辨对象是否初始化用
        uintptr_t weakly_referenced : 1;  // 有没有被弱引用过
        uintptr_t deallocating      : 1;  // 正在释放
        uintptr_t has_sidetable_rc  : 1;  // 0:引用计数器在isa中;1:引用计数器存在SideTable
        uintptr_t extra_rc          : 19; // 引用计数器-1
    };
}

有什么特殊的位:
在这里插入图片描述

Tagged Pointer的优化:

  1. Tagged Pointer专门用来存储小的对象,例如NSNumber, NSDate, NSString。

  2. Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。

  3. 在内存读取上有着3倍的效率,创建时比以前快106倍。

    例如:1010,其中最高位1xxx表明这是一个tagged pointer,而剩下的3位010,表示了这是一个NSString类型。010转换为十进制即为2。也就是说,标志位是2的tagger
    pointer表示这是一个NSString对象。

  1. isa指针里面都存了什么,32和64位分别讲一下

答:在ARM 32位的时候,isa的类型是Class类型的,直接存储着实例对象或者类对象的地址;

在ARM64结构下,isa的类型变成了共用体(union),使用了位域去存储更多信息

  1. OC 是否支持重载? 为什么?

答:不完全支持,参数个数不同的函数重载可以说是支持。但是参数相同、函数名相同的 编译不通过

  1. IMP、SEL Method 都表示什么意思? 与 _cmd 相关

答:SEL,方法选择器,本质上是一个C字符串

IMP,函数指针,函数的执行入口

Method,类型,结构体,里面有SEL和IMP

/// Method
struct objc_method {
    SEL method_name; 
    char *method_types;
    IMP method_imp;
 };
  1. class 的底层结构是什么样的?

答:typedef struct objc_class *Class;

objc_class里面有isa指针、superclass指针、方法缓存、具体的类信息

  1. method_t 里包含什么?

答:

struct method_t {
    SEL name; //函数名
    const char *types; //编码(返回值类型、参数类型)
    IMP imp;//指向函数的指针(函数地址)
};
  1. super 关键字的本质是什么?

答:super:是编译器指示符,仅仅是一个标志,并不是指针,仅仅是标志的当前对象去调用父类的方法,本质还是当前对象调用。

  1. OC的消息机制有几步?

答:消息发送

消息发送-objc_msgSend-01

消息发送

动态方法解析- objc-msgSend-02

消息转发-objc_msgSend-03

消息转发

  1. 如何防止类似 unrecognized selector 的错误?_objc_msgForward能干什么?

答:消息转发机制中三大步骤:消息动态解析消息接受者重定向消息重定向。通过这三大步骤,可以让我们在程序找不到调用方法崩溃之前,拦截方法调用。

第二步消息接受者重定向进行拦截,开销最小,适合重写。

  1. runtime 有哪些应用?方法替换(method - Swizzling)有什么缺点?如何安全的进行方法替换?

答:1、逆向。2、分类属性附加。3、预防崩溃等。4、方法的交换swizzling。5、快速定义归档和解档属性。

缺点:

1、找不到真正的方法归属

例如数组越界,你以为替换的是NSArray的方法,事实上是_NSArrayI的方法

2、多次进行方法交换,会将方法替换为原来的实现

解决:利用单利进行限制,只进行一次方法交换

3、交换的旧方法,子类未实现,父类实现

出现的问题:父类在调用旧方法时,会崩溃
解决方法:先进行方法添加,如果添加成功则进行替换<repleace>,反之,则进行交换<exchange>

4、进行交换的方法,子类、父类均未实现

出现的问题:出现死循环 解决方法:如果旧方法为nil,则替换后将swizzeldSEL复制一个不做任何操作的空实现。

5、类方法–类方法存在元类中。

答:找错替换的目标了

  1. person有个+test方法,实现输出persion test,student继承persion,头文件定义-test方法,但没实现,student *obj=new student;[obj test]; 结果是啥?

答:闪退

  1. 介绍下 Swizzle 的步骤? 具体到方法名.

答:具体到方法名有病,如果谁出这种题目的,就鄙视他

  1. Swizzle 时, 我不想替换父类, 只想替换子类,怎么办?

答:先判断子类是否存在该方法,不存在就添加方法,这样Swizzle时候因为方法已经存在了,就不会上溯到父类

  1. Swizzle 的优缺点? 缺点会导致什么问题?

答:同2。

1,只交换一次
2,获取方法(class_getInstanceMethod/class_getClassMethod)会沿着继承者链向上寻找,所以防止交换时,交换的方法实现时本类的,绝对不能是父类的

  1. 方法交换和分类同时去hook同一个方法,结果会怎么样? 具体交换的是什么?交换时是如何处理传参数? 如果使用NSInvocation 的话, 是否能处理方法有返回值的场景?具体怎么处理的?

答:

引用计数、weak、autoreleasepool

  1. 引用计数怎么实现的?weak怎么实现的?sideTable的 底层结构是怎么样的? weak指针做了什么操作?

答:引用计数是管理对象生命周期的办法。在对象内部保存一个被引用次数的数字,init、new 和 copy 都会让+1,调用 release
让 -1。当等于 0 的时候,调用 dealloc 来销毁对象。引用计数分为ARC和MRC。

系统对每个有弱引用的对象,都维护一个sideTable表来记录它所有的弱引用的指针地址。当一个对象的引用计数为 0
时,系统就通过这张表,找到所有的弱引用指针,继而把它们都置成 nil。

sideTable的 底层结构:

spinlock_t slock // 自旋锁,用于上锁/解锁 SideTable
RefcountMap refcnts // 用来存储OC对象的引用计数的 hash表 (仅在未开启isa优化或在isa优化情况下isa_t的引用计数溢出时才会用到)
weak_table_t weak_table // 存储对象弱引用指针的 hash表 。是OC中weak功能实现的核心数据结构。

weak指针做了什么操作?

NSObject *obj = [[NSObject alloc] init];
__weak id weakObj = obj;
/*
1、会通过obj从sidetables中找到sidetable
2、找到sidetable中的弱引用表weak_table_t
3、通过obj从weak_table_t中的weak_entries找到obj对应的weak_entry_t
4、在obj对应的weak_entry_t的weak_referrer_t中加入weakObj指针
当obj释放时,会判断obj的weakly_referenced是否为1,即obj是否被弱引用。如果被弱引用,则进行下面的操作
1、会通过obj从sidetables中找到sidetable
2、找到sidetable中的弱引用表weak_table_t
3、通过obj从weak_table_t中的weak_entries找到obj对应的weak_entry_t
4、查找weak_entry_t中的weak_referrer_t数组,并将weak_referrer_t中存储的指针(这里指weakObj)指向nil
*/
  1. 对象的 release 是怎么处理的?

答:release方法主要是处理引用计数;由于isa有优化(arm64架构)和未优化之分,引用计数存储的位置不同,所以这其中包括对不同情况的判断;但总的逻辑就是引用计数减1,然后判断引用计数是否为0,如果为0则调用dealloc方法,释放对象

总的来说,dealloc方法做的处理:清空引用计数,清除weak弱引用指针,删除关联对象

  1. 堆和栈的区别是什么?

答:堆:先进先出,栈:先进后出

从管理方式区分,堆:手动释放内存,栈:自动释放内存

从空间大小,堆:空间大速度慢,栈:空间小速度快

  1. 栈、堆分别是否会被线程所共享?

答:在多线程环境中,多个线程会共享堆上的内存,为了确保线程安全,不得不在堆上进行加锁操作,但是加锁操作是很耗费性能的,你在堆上所获的的数据安全性实际上是在牺牲性能的代价下得来的。

  1. 内存空间中除了堆和栈还有什么内容?

答:堆区、栈区、全局区、常量区、代码区

  1. weak 如何把 对象重制为 nil

答:runtime 维护了一个Weak表,weak_table_t
用于存储指向某一个对象的所有Weak指针。Weak表其实是一个哈希表, key是所指对象的地址,value是weak指针的地址的数组。
在对象回收的时候,就会在weak表中进行搜索,找到所有以这个对象地址为键值的weak对象,从而置位nil。

  1. assign、strong 区别, 是否能用assign修饰 NSObject?

答:assign引用计数器不变,一般用于基础数据类型,strong引用计数器+1,用于对象,可以

  1. AutoReleasePool(自动释放池) 的底层实现是什么?他怎么实现及时释放的?子线程的释放时机是怎么样的?

答:一个线程的autoreleasepool就是一个指针栈。栈中存放的指针指向加入需要release的对象。

当自动释放池销毁时自动释放池中所有对象作release操作。

每个线程创建的时候就会创建一个autorelease
pool,所以子线程的autorelease对象,要么在子线程中设置runloop清除,要么在线程退出的时候,清空autorelease
pool

内存检测、OOM

  1. ARC下哪些情况会造成内存泄漏

答:

1、Delegate循环引用

2、Block循环引用

3、performSelector动态调用

  1. 内存泄漏如何检测?

答:

1、静态分析

2、Leak

3、Allocation

4、MLeaksFinder

  1. -OOM (Out Of Memory) 类型的 crash介绍下, 怎么检测, 怎么处理?

答:OOM低内存崩溃。

Memory Graph工具,内存快照机制。

OOMDetector

  1. dealloc __weak会有什么问题

答:崩溃。__weak
会调用weak_register_no_lock,里边有对于当前实例是否处在deallocating状态(正在被释放的状态)的判断,如果处在deallocating状态,则crash

  1. 说一下iOS内存分区情况

答:堆区、栈区、全局区、常量区、代码区

分类、扩展、关联对象

  1. Catagory 和 extension 分别的使用场合和特点是什么?

答:Extension(扩展)与Category(分类)区别。

生效周期:Category是runtime,Extension是编译时。

Category可以为系统类添加分类,Extension不能。

文件:Category是有h声明和m实现,Extension直接写在宿主.m文件,只有h声明。

Category只能扩充方法,不能扩充成员变量和属性,因为成员变量是编译时的,属性会在编译时自动生成setter和getter,但Category是runtime。

  1. Catagory 的实现原理是什么?Catagory 有哪些用处?Catagory 有什么局限?

答:(原理,不要背,理解为主)
编译时都会生成一个结构体并将分类的方法列表等信息存入这个结构体。在编译阶段分类的相关信息和本类的相关信息是分开的。等到运行阶段,会通过runtime加载某个类的所有Category数据,把所有Category的方法、属性、协议数据分别合并到一个数组中,然后再将分类合并后的数据插入到本类的数据的前面.
用途:
1.可以减少单个类的体积,降低耦合性,同一个类可以多人进行开发。
2.可以为系统类添加分类进行拓展。
3.模拟多继承。
4.把静态库的私有方法公开。

局限:
能间接实现属性,但不能定义成员变量,成员变量是在编译时,不在运行时,而分类是在运行时生效的。

  1. Class 和 他的 Catagory 同名方法的调用顺序是什么?Catagory A 和 Catagory B 同名方法的调用顺序是如何?如果想要不按照系统顺序执行要怎么做?

答:同下。同一主类的不同分类中的普通同名方法调用, 取决于编译的顺序, 后编译的文件中的同名方法会覆盖前面所有的,包括主类.
+load方法的顺序也取决于编译顺序, 但是不会覆盖。

  1. +load 和 +initialize 的调用时机和顺序?两者区别是什么?

答:总结

  1. 普通方法的优先级: 分类> 子类 > 父类, 优先级高的同名方法覆盖优先级低的
  2. +load方法的优先级: 父类> 子类> 分类
  3. +load方法是在main() 函数之前调用,所有的类文件都会加载,包括分类
  4. +load方法不会被覆盖
  5. 同一主类的不同分类中的普通同名方法调用, 取决于编译的顺序, 后编译的文件中的同名方法会覆盖前面所有的,包括主类. +load方法的顺序也取决于编译顺序, 但是不会覆盖
  6. 分类中的方法名和主类方法名一样会报警告, 不会报错
  7. 声明和实现可以写在不同的分类中, 依然能找到实现
  8. 当第一次用到类的时候, 如果重写了+ initialize方法,会去调用
  9. 当调用子类的+ initialize方法时候, 先调用父类的,如果父类有分类, 那么分类的+ initialize会覆盖掉父类的, 和普通方法差不多
  10. 父类的+ initialize不一定会调用, 因为有可能父类的分类重写了它
  1. Catagory 有 +load 方法么?+load 是什么时候调用的?能继承么?会覆盖Class 的 +load 么?

答:+load方法是在main() 函数之前调用,所有的类文件都会加载,包括分类。+load方法的优先级: 父类> 子类>
分类。+load方法不会被覆盖。

  1. Catagory关联对象(AssociateObject)的底层实现是什么?

答:

总结:

  1. 通过关联管理类 AssociationsManagerget 函数取得一个全局唯一关联哈希表 AssociationsHashMap
  2. 根据我们的原始对象的伪装整数key DisguisedPtr 从关联哈希表 AssociationsHashMap 取得对象关联哈希表 ObjectAssociationMap
  3. 根据我们指定的关联 key ( const void *key ) 从 对象关联哈希表ObjectAssociationMap 取得对象关联model ObjcAssociation
  4. 对象关联modelObjcAssociation 的两个成员变量,保存我们的关联策略 _policy 和关联值 _value

在分类中到底能否实现属性?首先要知道属性是什么,属性的概念决定了这个问题的答案。

  • 如果把属性理解为通过方法访问的实例变量,那这个问题的答案就是不能,因为分类不能为类增加额外的实例变量。
  • 如果属性只是一个存取方法以及存储值的容器的集合,那么分类可以实现属性。 分类中对属性的实现其实只是实现了一个看起来像属性的接口而已。
  1. 方法如果写了多个分类、会执行哪一个?执行逻辑是什么样?

答:同一主类的多个分类中的普通同名方法调用, 取决于编译的顺序, 后编译的文件中的同名方法会覆盖前面所有的,包括主类。

  1. 关联对象 weak 底层原理

答:了解即可,靠脑子是不能详细记住的,大概为对象底层的弱引用表的作用。

1、static id storeWeak(id *location, objc_object *newObj)

该函数总体来说做了2件事:

  • 如果该weak指针有指向的旧对象oldObj,则根据oldObj拿到所在的弱引用表oldTable,然后从oldTable->weak_table中清除掉当前weak指针。
  • 如果有设置新对象newObj,则根据newObj拿到所在的弱引用表newTable,然后将当前weak指针写入newTable->weak_table。再标记新对象已经被弱引用,最后赋值,返回新值。

2、weak_unregister_no_lock

该函数有三个核心步骤:

  • weak_entry_t *entry = weak_entry_for_referent(weak_table, referent),从weak_table-> weak_entries中通过referent,拿到referent的引用实体。
  • 从entry->referrers中移除weak指针
  • 如果entry->referrers中的weak指针全部移除了(空了),那么将当前的entry从weak_table中移除。

3、weak_register_no_lock

该函数有三个核心步骤:

  • weak_entry_t *entry = weak_entry_for_referent(weak_table, referent))从weak_table->weak_entries中通过referent,拿到referent的引用实体

  • 如果entry存在,则直接将weak指针插入到entry->referrers中。

  • 如果entry不存在,则新建一个new_entry,将referent与 referrer关联,插入到weak_table-> weak_entries中。(插入之前会按需扩容)。

NSMutableArray扩展

  1. [mutablearry alloc]init 和 [nsmublearray array]有什么区别

答:[[NSMutableArray alloc]init] alloc分配内存,init初始化,需要手动释放
[NSMutableArray array] 不需要手动release,遵循autoreleasepool机制

在ARC(自动引用计数)中两种方式并没什么区别

  1. 结构体中为什么不能使用oc对象

答:结构体,生效周期是编译时,oc对象是运行时,结构体在栈区,oc对象在堆区

  1. 我们在开发中使用文件的.mm是基于什么原因?

答:C++混编

  1. string和NSString的区别

答:NSString是string的OC封装,但是NSString是一个内存优化的结果,OC中的NSString不论是在编译时还是在运行时都做了很多的优化,并不同于普通的对象,它是一个非常复杂的存在。

NSString有三个类型
__NSCFConstantString//常量字符串,如果值一样,无论写多少遍,都是同一个对象。可以直接用 == 来比较
__NSCFString//对象类型的字符串,运行时堆区服从对象内存管理策略,长度太小会转化为NSTaggedPointerString NSTaggedPointerString//长度0-9,对于64位程序,为了节省内存和提高运行速度,是对__NSCFString的优化

TaggedPointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,OC
对象的内存管理方式对其无效。

  1. mutablearray是怎么实现的,mutablearray申请内存空间干什么用,做增删操作的时候内存空间是怎么改变的,可以用别的方法实现吗?

答:

offset: 有效数据起始位置偏移量

size: 实际占用的内存大小

used: 数组的实际的有效数据个数

不需要改动内存,用偏移量offset来记录数据的位置。

如果buff的size还够用,不需要扩展buff,数据会在buff的末端添加进去,此时offset由0变成size-1,used+1.over循环buff的牛逼之处就在于此,无需移动内存,实现插入元素。

总结:

1.数组越界奔溃: index > _used+offset 或 index < 0。

2.如果想要内存记录释放,remove之后记得置nil.或者直接置nil.猜想Array的dealloc的方法会自动给所有元素发release消息。

Timer、锁

  1. 你知道 iOS 有哪些锁?性能分别怎么样?

答:11种。

性能从强到弱

OSSpinLock,信号量,pthread_mutex,NSLock,pthread_rwlock,os_unfair_lock,pthread_mutex(recursive),NSRecursiveLock,NSConditionLock,@sychronized

  1. NSTimer、CADisplayLink、dispatch_source_t 的优劣

答:

NSTimer:

优点: 方便使用 缺点: 计时不精确,容易造成循环引用

CADisplayLink:

优点: 只要设备屏幕刷新频率保持在60fps,那么其触发时间上是最准确的。 缺点:
由于依托于屏幕刷新频率,如果CPU任务过重,那么会影响屏幕刷新,触发事件也会受到相应影响。

dispatch_source_t:

优点:
非常精确,要想达到百分比精确需要单独拥有一个队列,只处理time相关的任务,并且time的回调处理时长不能操过设置的时间间隔。

缺点: 不受RunLoop的影响,需要注意内存管理

  1. 自旋锁和互斥锁怎么选择?

答:

自旋锁:

1、时间短

2、加锁的代码(临界区)经常被调用,但竞争情况很少发生

3、CPU资源不紧张

4、多核

互斥锁:

1、时间长

2、单核

3、临界区有IO操作

4、临界区代码复杂或者循环量大

5、临界区竞争非常激烈

  1. NSNotificationCenter 跨线程及底层结构是怎样的?

答:总结:接收通知和发送通知时所在线程一致,和监听时所在线程无关。

  1. 读写锁思路、手写一下

答:是什么导致的会crash呢?在读取的时候,数据变了,或者对象给释放了。

  • 条件1: 同一时间,只能有1个线程进行写的操作
  • 条件2: 同一时间,允许有多个线程进行读的操作
  • 条件3: 同一时间,不允许既有写的操作,又有读的操作
  1. atomic与@synchroize原理

答:atomic原子性,setter和getter加了自旋锁(实则是互斥锁)。@synchronized是一个互斥递归锁。

什么是自旋锁呢?

锁用于解决线程争夺资源的问题,一般分为两种,自旋锁(spin)和互斥锁(mutex)。

互斥锁可以解释为线程获取锁,发现锁被占用,就向系统申请锁空闲时唤醒他并立刻休眠。互斥锁加锁的时候,等待锁的线程处于休眠状态,不会占用CPU的资源

自旋锁比较简单,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。自旋锁加锁的时候,等待锁的线程处于忙等状态,并且占用着CPU的资源。

原子操作的颗粒度最小,只限于读写,对于性能的要求很高,如果使用了互斥锁势必在切换线程上耗费大量资源。相比之下,由于读写操作耗时比较小,能够在一个时间片内完成,自旋更适合这个场景。

差点被苹果骗了!原来系统中自旋锁已经全部改为互斥锁实现了,只是名称一直没有更改。

为了修复优先级反转的问题,苹果也只能放弃使用自旋锁,改用优化了性能的 os_unfair_lock,实际测试两者的效率差不多。

os_unfair_lock用于取代不安全的OSSpinLock,从iOS10开始才支持

从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等

TCP/IP协议族

  1. HTTP、HTTPS 区别?

答:HTTPS是HTTP的安全版,在HTTP的基础上加入SSL层,对数据传输进行加密和身份验证

  1. GET、POST 请求的 cache 怎么做,几级缓存? 着重讲本地缓存? 缓存有效期怎么做的?内部缓存机制的优化机制?如何防止内存、磁盘的缓存爆掉?

答:NSURLCache,设置内存、磁盘的缓存的大小

  1. HTTP 请求方法种类有哪些?(别忘记HEAD)

答:HTTP 请求方法有:GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS、TRACE。最常用的就是 GET
方法 和 POST 方法;

方法说明
GET获取资源
POST传输实体主体
PUT传输文件
HEAD获得报文首部
DELETE删除文件
OPTION询问支持的方法
TRACE追踪路径
CONNECT要求用隧道协议连接代理
LINK建立和资源之间的联系
  1. TCP流量控制

答:发送方不能无脑的发送数据给接收方,要考虑接收方的处理能力。

如果一直无脑的发送数据给对方,但是对方处理不过来,那么就会触发重传机制,从而导致网络流量无端的浪费。

为了解决这种现象发生,TCP提供一种机制可以让【发送方】根据【接收方】的实际接收能力来控制发送的数据量,这就是所谓的流量控制。

  1. HTTPS的握手过程

答:

1.客户端发送出协议版本号,一个客户端生成的随机数,以及客户端支持的加密方法。

2.服务端确认双方使用的加密方法,并给出数字证书以及一个服务器生成的随机数。

3.客户端确认数字证书有效,然后生成一个新的随机数,并使用数字证书中的公钥加密这个随机数,发送给服务端。

4.服务端使用自己的私钥,解密出随机数。

5.客户端和服务端根据约定的加密方法,使用前面的三个随机数生成对话密钥,用来加密接下来的对话。

  1. HTTPS与HTTP的区别? 非对称加密、对称加密都是在哪一个步骤?

答:HTTPS是HTTP的安全版,在HTTP的基础上加入SSL层,对数据传输进行加密和身份验证。

对称加密来传送消息,但对称加密所使用的密钥我们可以通过非对称加密的方式发送出去。

  1. DNS、工作在什么层、默认端口?

答:DNS工作在应用层,默认端口为53

  1. Ping原理

答:采用UICMPPacket对象封装,ping的原理是用类型码为8的ICMP发请求,收到请求的主机则用类型码为0的ICMP回应。通过计算ICMP应答报文数量和与接受与发送报文之间的时间差,判断当前的网络状态。

计算方法:ping命令在发送ICMP报文时将当前的时间值存储在ICMP报文中发出,当应答报文返回时,使用当前时间值减去存放在ICMP报文数据中存放发送请求的时间值来计算往返时间。ping返回接收到的数据报文字节大小、TTL值以及往返时间。

  1. 证书信息相关

答:乱七八糟,都不知道在问什么。

二叉树、排序

  1. 堆的数据结构

答:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

  1. 二叉搜索树的作用

答:二叉树的提出其实主要就是为了提高查找效率。

  1. 层序遍历也叫什么遍历,怎么实现

答:广度优先遍历(BFS)。

  1. 二叉树中增加节点

答:

  1. 堆排序、归并排序、快排原理,优缺点

答:

  1. 二叉树反转, 数组形式

答:

性能优化

  1. 造成tableView卡顿的原因有哪些?

答:

1.最常用的就是cell的重用

注册重新标识符 如果是重用cell时,每当cell显示到屏幕上时,就会重新创建一个新的cell;如果有很多数据的时候,就会堆积很多cell。
如果重用cell,就为cell创建一个ID,每当需要显示cell 的时候,都会先去缓冲池中寻找可循环的cell,如果没有再重新创建cell.

2.避免cell的重新布局

cell的布局填充等操作比较耗时,一般创建时就布局好,如可以将cell单独放到一个自定义类,初始化时就布局好.

3.提前计算并缓存cell的属性及内容

当我们创建cell的数据源方法时,编译并不是先创建cell 再定cell的 高度,
是先根据内容依次确定每个cell的高度,高度确定后,再创建要显示的
cell,滚动时,每当cell进入都会计算高度,提前估算高度告诉编译,编译知道高度后,紧接着就会创建cell,这时再调高度的具体计算方法,这样可以不浪费时间去计算显示以外的cell

4.减少cell中控件的数,尽量使cell得布局相同,同格的cell可以使的重 标识符,初始化时添加控件,适当的可以先隐藏.

5.要使用ClearColor,背景,透明度也要设置为0 渲染耗时较大

6.使用局部刷新 ,如果只是新某组的话,使 reloadSection进行局部刷新

7.加载网络数据,下载图,使用异步加载,并缓存

8.少使 addView 给cell动态添加view

9.按需加载cell,cell滚动很快时,只加载范围内的cell

10.要实现的代理方法,tableView只遵守两个协议,不用的代理方法可以不写.

11.缓存 :estimatedHeightForRow 能和HeightForRow 的 layoutIfNeed同时存在,这两者同时存在才会出现“窜动”的bug。所以我的建 议是:只要是固定 就写预估 来减少 调
次数提升性能。如果是动态的就要写预估算法 ,每个的缓存字典来减少代码的调用次数即可.

12.要做多余的绘制 作。 在实现drawRect:的时候,它的rect参数就是需要绘制的区域,这个区域之外的 需要进 绘制.如上 中,就可以 CGRectIntersectsRect、CGRectIntersection或
CGRectContainsRect判断是否需要绘制image和text,然后再调绘制方法。

13.预渲染图像.当新的图像出现时,仍然会有短暂的停顿现象。解决的办法就是在bitmap context 先将其画 遍,导出成UIImage对象,然后再绘制到屏幕;

14.使用正确的数据结构来存储数据。

  1. APP启动时间应从哪些方面优化?

答:- 应用启动时是用怎样加载所有依赖的Mach-O文件的?

  • 请列举你所知道main()函数之前耗时的因素都有哪些

App启动分为两种:

  • 冷启动(Cold Launch):从零开始启动app
  • 热启动(Warm Launch):app已在内存中,在后台存活,再次点击图标启动app

启动时间的优化,主要是针对冷启动进行优化 1、通过添加环境变量可以打印app的启动时间分析(详情请见下图)

  • DYLD_PRINT_STATISTICS
  • DYLD_PRINT_STATISTICS_DETAILS(比上一个详细)
  • 一般400毫秒以内正常

打印结果:

Total pre-main time: 238.05 milliseconds (100.0%)// main函数调用之前(pre-main)总耗时
          dylib loading time: 249.65 milliseconds (104.8%)// 动态库耗时 
          rebase/binding time: 126687488.8 seconds (18128259.6%) 
          ObjC setup time:  10.67 milliseconds (4.4%)// OC结构体准备耗时 
          initializer time:  52.83 milliseconds (22.1%)// 初始化耗时 
          slowest intializers :// 比较慢的加载 
          libSystem.B.dylib :   6.63 milliseconds (2.7%)    
          libBacktraceRecording.dylib :   6.61 milliseconds (2.7%)
          libMainThreadChecker.dylib :  31.82 milliseconds (13.3%)
     

2、冷启动可以概括为3大阶段

  • dyld
  • runtime
  • main

3、dyld(dynamic link editor),Apple的动态连接器,可以装载Mach-O(可执行文件、动态库等)

  • 装载app的可执行文件,同时递归加载所有依赖的动态库
  • 当dyld把可执行文件、动态库都装载完成后,会通知runtime进行下一步处理

4、runtime所做的事情

  • 调用map_images函数中调用call_load_methods,调用所有Class和Category的+load方法
  • 进行各种objc结构的初始化(注册objc类、初始化类对象等等)
  • 调用C++静态初始化器和__attribure__((constructor))修饰的函数(JSONKit中存在具体应用)
  • 到此为止,可执行文件和动态库中所有的符号(Class, Protocol, Selector, IMP…)都已按格式成功加载到内存中,被runtime所管理

5、总结

  • app的启动由dylb主导,将可执行文件加载到内存,顺便加载所有依赖的动态库
  • 并由runtime负责加载成objc定义的结构
  • 所有初始化工作结束后,dyld就会调用main函数
  • 接下来就是ApplicationMain函数,AppDelegate的application:didFinishLaunchingWithOptions:方法

6、按照不同的阶段优化 dyld

  • 减少动态库、合并一些动态库(定期清理不必要的动态库)
  • 减少objc类、分类的数量、减少selector数量(定期清理不必要的类、分类)
  • 减少C++虚构函数
  • Swift尽量使用struct

runtime

  • 使用+initialize方法和dispatch_once取代所有的__attribute__((constructor))、C++静态构造器、Objc的+load方法

main

  • 在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在finishLaunching方法中
  • 按需加载
  1. 卡顿掉帧优化策略?

答:

由于是因为CPU和GPU工作耗时导致的卡顿掉帧,所以可以从CPU和GPU两方面来进行优化,减轻两者的耗时工作。

  • CPU:将CPU的工作放置到子线程完成。如对象的创建、调整、销毁;layout布局计算、文本计算;文本的异步绘制、图片编解码。

  • GPU:避免离屏渲染。视图的圆角设置、阴影、蒙版、光栅化都会造成离屏渲染增加GPU工作量,可通过CPU的异步绘制机制来完成这类操作,从而减轻GPU压力。

算法

  1. 实现一个算法,计算100的阶乘

答:尽量不要使用递归,会导致方法栈空间占用过大。所以采用for循环的方式进行计算就OK

long long dofactorial(int max){
    long long result = 1;
    for (int i = 0; i  INT_MAX){
            //考虑溢出
            return -1;
        }
    }
}
int main(int argc, const char * argv[]) {
    int result = dofactorial(100);
    printf("result = %lld", result);
    return 0;
}
  1. 编程实现字符串拷贝,要考虑下内存重叠问题

答:

char *memcpy_qi(char *dst, const char* src)
{
    int cl = strlen(src)+1;
    char *ret = dst;
    if (dst >= src && dst <= src+ cl-1) //内存重叠,从高地址开始复制
    {
        //挪开空间
        dst = dst+ cl-1;
        //将指针挪到结尾
        src = src+ cl-1;
        while (cl—)
            *dst— = *src—;
    }
    else    //正常情况,从低地址开始复制
    {
        while (cl—)
            *dst++ = *src++;
    }
    return ret;
}
char * strcpy_qi(char *dst,const char *src)
{
    assert(dst != NULL && src != NULL);
    char *ret = dst;
    memcpy_qi(dst, src);
    return ret;
}
  1. 如何求两个View的最近公共父类

答:
1、典型的倒Y字型链表组合,减少遍历次数最好从最短链表开始。
2、公共节点之后的所有节点都是一样的,最后的一个必定是NSObject,可以忽略。

- (void)viewDidLoad {
    [super vie