面试2021

最全

iOSInterviewsAndDevNotes

零散

21出一套iOS高级面试题2018年7月.md
阿里、字节 一套高效的iOS面试题解答
iOS面试了20几家总结出来的面试题(一)
2020年面试:整理出一份高级iOS面试题
2019 iOS 面试题大全(补充完整版)
2020年6月最新iOS面试题总结(答案篇)
20阿里字节一套高效的iOS面试题2020年2月.md
基础知识,操作系统,网络

待补充

lru缓存

哈希桶

链式地址法和开放地址法的优缺点分别是什么?
链式地址法(HashMap)

优点:

处理冲突简单,且无堆积现象,平均查找长度短;
  链表中的结点是动态申请的,适合构造表不能确定长度的情况;
  相对而言,拉链法的指针域可以忽略不计,因此较开放地址法更加节省空间。
  插入结点应该在链首,删除结点比较方便,只需调整指针而不需要对其他冲突元素作调整。

缺点:
  指针占用较大空间时,会造成空间浪费。若空间用于增大散列表规模进而提高开放地址法的效率。

开放地址法:
缺点:
  容易产生堆积问题;
  不适于大规模的数据存储;
  结点规模很大时会浪费很多空间;
  散列函数的设计对冲突会有很大的影响;
  插入时可能会出现多次冲突的现象,删除的元素是多个冲突元素中的一个,需要对后面的元素作处理,实现较复杂;

优点:
  当节点规模较少,或者装载因子较少的时候,使用开发寻址较为节省空间,如果将链式表的指针用于扩大散列表的规模时,可使得装载因子变小从而减少了开放寻址中的冲突,从而提高平均查找效率。

作者:沈先生的影子
链接:https://www.jianshu.com/p/4de541a56c03
来源:
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

阿里字节一套高效的iOS面试题2020年2月

启动顺序

OS的启动流程

根据 info.plist 里的设置加载闪屏,建立沙箱,对权限进行检查等
加载可执行文件
加载动态链接库,进行 rebase 指针调整和 bind 符号绑定
Objc 运行时的初始处理,包括 Objc 相关类的注册、category 注册、selector 唯一性检查等;
初始化,包括了执行 +load() 方法、attribute((constructor)) 修饰的函数的调用、创建 C++ 静态全局变量。
执行 main 函数
Application 初始化,到 applicationDidFinishLaunchingWithOptions 执行完
初始化帧渲染,到 viewDidAppear 执行完,用户可见可操作。

介绍下runtime的内存模型(isa、对象、类、metaclass、结构体的存储信息等)

对于objc1.0来说

breme大神讲解

struct objc_class {
  struct objc_class *isa;
};
struct objc_object {
  struct objc_class *isa;
};

typedef struct objc_class *Class; //类  (class object)
typedef struct objc_object *id;   //对象 (instance of class)

凡是有isa指针的,都可以称做一个对象
对象(objc_object)的isa指针指向类对象(objc_class),类对象的isa指针指向metaclass(objc_class),metaclass的isa指针指向NSObject的meta_class,NSObject的meta_class指向自己。可以总结为,一切皆为objc_class


image.png
image.png

贴个breme大神的代码,将objc转成C++,可以看到,创建一个类,会创建OBJC_CLASS__NyanCat ,分别对应类和元类,他们都是_class_t

//Class的实际结构
struct _class_t {
    struct _class_t *isa;        //isa指针
    struct _class_t *superclass; //父类
    void *cache;
    void *vtable;
    struct _class_ro_t *ro;     //Class包含的信息
};
 
//Class包含的信息
struct _class_ro_t {
    unsigned int flags;
    unsigned int instanceStart;
    unsigned int instanceSize;
    unsigned int reserved;
    const unsigned char *ivarLayout;
    const char *name;                                 //类名
    const struct _method_list_t *baseMethods;         //方法列表
    const struct _objc_protocol_list *baseProtocols;  //协议列表
    const struct _ivar_list_t *ivars;                 //ivar列表
    const unsigned char *weakIvarLayout;
    const struct _prop_list_t *properties;            //属性列表
};
 
 //NyanCat(meta-class)
struct _class_t OBJC_METACLASS_$_NyanCat  = {
    .isa        = &OBJC_METACLASS_$_NSObject,
    .superclass = &OBJC_METACLASS_$_NSObject,
    .cache      = (void *)&_objc_empty_cache,
    .vtable     = (void *)&_objc_empty_vtable,
    .ro         = &_OBJC_METACLASS_RO_$_NyanCat, //包含了类方法等
};
 
//NyanCat(Class)
struct _class_t OBJC_CLASS_$_NyanCat = {
    .isa        = &OBJC_METACLASS_$_NyanCat,   //此处isa指向meta-class
    .superclass = &OBJC_CLASS_$_NSObject,
    .superclass = (void *)&_objc_empty_cache,
    .vtable     = (void *)&_objc_empty_vtable,
    .ro         = &_OBJC_CLASS_RO_$_NyanCat,   //包含了实例方法 ivar信息等
};
 
typedef struct objc_object NyanCat;   //定义NyanCat类型
//更详细的不贴代码了..
对于objc2.0来说

Runtime内存模型探究

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    class_rw_t *data() const {
        return bits.data();
    }
}

struct objc_object {
private:
    isa_t isa;
}

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    Class cls;
    uintptr_t bits;
}

2.0版本的内存模型,objc_object里的isa指针不是Class类型,是个isa_t的结构体,但是isa_t结构体里还是包含了Class指针

class_data_bits_t

1.0里Class的_class_ro_t替换成了class_data_bits_t里的class_rw_t,里面也是有方法列表和属性列表

struct class_data_bits_t {
    friend objc_class;

    // Values are the FAST_ flags above.
    uintptr_t bits;
    
    class_rw_t* data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
}

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint16_t version;
    uint16_t witness;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
}

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    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_copyIvarList & class_copyPropertyList区别

# class_copyIvarList方法获取实例变量问题引发的思考

class_copyIvarList获取当前类的实例变量,但不包括父类的,json转模型很多用到这个方法
class_copyPropertyList获取用property定义的属性名,也可以用来json转模型。

题解答案从底层源码(objc2.0)出发
class_copyIvarList是拿的class_rw_t里的ro(class_ro_t)的ivars属性
class_copyPropertyList拿的是class_rw_t里的properties属性

Ivar *
class_copyIvarList(Class cls, unsigned int *outCount)
{
    const ivar_list_t *ivars;
    Ivar *result = nil;
    unsigned int count = 0;

    if (!cls) {
        if (outCount) *outCount = 0;
        return nil;
    }

    mutex_locker_t lock(runtimeLock);

    assert(cls->isRealized());
  //class_copyIvarList是拿的class_rw_t里的ro(class_ro_t)的ivars属性
    if ((ivars = cls->data()->ro->ivars)  &&  ivars->count) {
        result = (Ivar *)malloc((ivars->count+1) * sizeof(Ivar));

        for (auto& ivar : *ivars) {
            if (!ivar.offset) continue;  // anonymous bitfield
            result[count++] = &ivar;
        }
        result[count] = nil;
    }

    if (outCount) *outCount = count;
    return result;
}

objc_property_t *
class_copyPropertyList(Class cls, unsigned int *outCount)
{
    if (!cls) {
        if (outCount) *outCount = 0;
        return nil;
    }

    mutex_locker_t lock(runtimeLock);

    checkIsKnownClass(cls);
    assert(cls->isRealized());

    auto rw = cls->data();

    property_t **result = nil;
    unsigned int count = rw->properties.count();
    if (count > 0) {
        result = (property_t **)malloc((count + 1) * sizeof(property_t *));

        count = 0;
        for (auto& prop : rw->properties) {
            result[count++] = ∝
        }
        result[count] = nil;
    }

    if (outCount) *outCount = count;
    return (objc_property_t *)result;
}

对象模型

OC对象占用内存原理
既然类方法都存在Class里,那oc的属性指向真正的对象的指针都存在哪里呢?其实在对象alloc的时候,所有指向指针的指针都分配在了objc_object这结构体后面的若干offset后面

image.png

weak实现

OC Runtime之Weak(2)---weak_entry_t

OC Runtime之Weak(1)---weak_table_t
OC对象之旅 weak弱引用实现分析

全局SideTables()->side table->weak table--->weak entry---> referrers + inline_referrers

设置weak对象的时候,根据对象的地址在全局SideTables()(哈希表)里,去除对应的side table(其实也算一个自定义的哈希表)

image.png
image.png

category如何被加载的,两个category的load方法的加载顺序,两个category的同名方法的加载顺序

美团category
category会在runtime启动_objc_init的时候加载(此时main函数还没执行),在_read_images函数里

category的加载是在运行时发生的,加载过程是,把category的实例方法、属性、协议添加到类对象上。把category的类方法、属性、协议添加到metaclass上。

category的load方法执行顺序是根据类的编译顺序决定的,即:xcode中的Build Phases中的Compile Sources中的文件从上到下的顺序加载的。

category并不会替换掉同名的方法的,也就是说如果 category 和原来类都有 methodA,那么 category 附加完成之后,类的方法列表里会有两个 methodA,并且category添加的methodA会排在原有类的methodA的前面,因此如果存在category的同名方法,那么在调用的时候,则会先找到最后一个编译的 category 里的对应方法。

initialize && Load

深入详解 iOS的 +load和+initialize

load调用时机和顺序

一、+load 方法是在所有类被加入到runtime以后,main函数执行之前被系统自动调用的。
二、系统自动为每一个类调用+load方法(如果有),无需手动调用,也无需手动调用[super load]。
三、+load方法会按照文件所在的Compile Sources顺序加载,在调用类的+load之前,会优先调用其父类的+load方法。
四、在所有类的+load方法调用完以后再调用[Category load]方法,加载顺序按照Compile Sources排列顺序。

+initialize调用时机和顺序

一、+ initialize 在类第一次接收到消息之前被系统自动调用,无需手动调用。
二、在调用子类的+ initialize 方法之前,会先调用父类的+ initialize 方法(如果有),所以也无需手动调用[super initialize]方法。
三、如果父类中有+ initialize方法,而子类中没有+ initialize方法,子类会自动继承父类的+ initialize方法,也就是说父类的+ initialize方法会调用两次。
四、Category中+ initialize方法会覆盖类中的+ initialize,同一个类有多个Category都实现了+initialize方法时,Compile Sources 列表中最后一个Category 的+initialize方法会覆盖其他的+ initialize方法。

AutoReleasePool释放

AutoreleasePool的原理和实现
AutoReleasePool
在没有手动加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop
如果采取一些非cocoa创建的一些线程,将不会自动生成autoreleasepool给你,你需要手动去创建它。

image.png

AutoReleasePool包含push和pop操作每一个线程的 autoreleasepool 其实就是一个指针的堆栈;
每一个指针代表一个需要 release 的对象或者 POOL_SENTINEL(哨兵- 对象,代表一个 autoreleasepool 的边界);
一个 pool token 就是这个 pool 所对应的 POOL_SENTINEL 的内存地址。当这个 pool 被 pop 的时候,所有内存地址在 pool token 之后的对象都会被 release ;
这个堆栈被划分成了一个以 page 为结点的双向链表。pages 会在必要的时候动态地增加或删除;
Thread-local storage(线程局部存储)指向 hot page ,即最新添加的 autoreleased 对象所在的那个 page 。

Autoreleasepool是由多个AutoreleasePoolPage以双向链表的形式连接起来的,

Autoreleasepool的基本原理:在每个自动释放池创建的时候,会在当前的AutoreleasePoolPage中设置一个标记位,在此期间,当有对象调用autorelsease时,会把对象添加到AutoreleasePoolPage中,若当前页添加满了,会初始化一个新页,然后用双向量表链接起来,并把新初始化的这一页设置为hotPage,当自动释放池pop时,从最下面依次往上pop,调用每个对象的release方法,直到遇到标志位。 AutoreleasePoolPage结构如下

class AutoreleasePoolPage {
magic_t const magic;
id *next;//下一个存放autorelease对象的地址
pthread_t const thread; //AutoreleasePoolPage 所在的线程
AutoreleasePoolPage * const parent;//父节点
AutoreleasePoolPage *child;//子节点
uint32_t const depth;//深度,也可以理解为当前page在链表中的位置
uint32_t hiwat;
}

谈下iOS开发中知道的哪些锁?

不再安全的 OSSpinLock

  • @synchronized 性能最差,SD和AFN等框架内部有使用这个.它内部也是递归锁

  • NSRecursiveLock 和 NSLock :建议使用前者,避免循环调用出现死锁

  • OSSpinLock 自旋锁,存在的问题是:优先级反转问题,破坏了spinlock。优先级翻转,即当低优先级线程占用了资源,高优先级线程这时也需要这个资源,但是被低优先级的抢占了,但是spinlock会一直等待,即忙等待,不会让出时间片,导致高优先级线程一直占用cpu,低优先级线程没有时间片,这样就导致卡顿

  • dispatch_semaphore 信号量 : 保持线程同步为线程加锁

  • NSCondition,一般用于生产者消费者问题

iOS NSCondition生产者消费者问题

How do I use NSConditionLock? Or NSCondition
iOS多线程下的锁

如果缓冲区满了怎么办?
生产者-消费者问题理解

NSCondition *condition = [NSCondition new];


-(void) crateConsumenr{
  [condition lock];
      while(self.products.count == 0){
         NSLog(@"等待产品");
         [condition wait];
     }
    [self.products removeObject:0];
      NSLog(@"消费产品");
     [condition unlock];
}


-(void)createProducter{
   [condition lock];
     [self.products addObject:[[NSObject alloc] init]];
    NSLog(@"生产了一个产品");
    [condition signal];
    [condition unlock];
}

App 启动速度怎么做优化与监控

  1. main() 函数执行前;
  • 加载可执行文件(App 的.o 文件的集合);
  • 加载动态链接库,进行 rebase 指针调整和 bind 符号绑定;
  • Objc 运行时的初始处理,包括 Objc 相关类的注册、category 注册、selector 唯一性
    检查等;
  • 初始化,包括了执行 +load() 方法、attribute((constructor)) 修饰的函数的调用、创建C++ 静态全局变量。
    根据上面,减少动态库使用,如果数量多,尽可能合并,减少load方法执行任务
  1. main() 函数执行后;
  • 从功能上梳理出哪些是首屏渲染必要的初始化功能,哪些是 App 启动必要的初始化功能,而哪些是只需要在对应功能开始使用时才需要初始化的。梳理完之后,将这些初始化功能分别放到合适的阶段进行
  • main() 函数开始执行后到首屏渲染完成前只处理首屏相关的业务,其他非首屏业务的初始化、监听注册、配置文件读取等都放到首屏渲染完成后去做。
  1. 方法级别优化
  • Time Profiler
  • objc_msgSend 方法进行 hook

线程保活

三次握手,四次挥手

TCP好文

image.png

image.png

本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接。”。主要目的防止server端一直等待,浪费资源。

runloop

  1. runloop在被获取创建
  2. runloop由若干个mode,比如默认的defaultMode和UITrackingMode,这两个mode都标记为common mode,所以加入timer时,不加入到这两个mode,而是加入到common mode items里面,会被runloop直接加到标记为common mode的mode里(上面提到的两个都是)
struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};
 
struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set
    CFMutableSetRef _commonModeItems; // Set
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set
    ...
};
  1. runloop里面,系统自己创建了很多时间的obeserver,如_wrapRunLoopWithAutoreleasePoolHandler,负责每个循环创建和销毁autorelease pool
    又如_UIGestureRecognizerUpdateObserver手势事件和_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv UI绘制

  2. 可以看到上面的source0和source1,__IOHIDEventSystemClientQueueCallback是souce1,手势触发的回调

common mode items = {
 
        // source0 (manual)
        CFRunLoopSource {order =-1, {
            callout = _UIApplicationHandleEventQueue}}
        CFRunLoopSource {order =-1, {
            callout = PurpleEventSignalCallback }}
        CFRunLoopSource {order = 0, {
            callout = FBSSerialQueueRunLoopSourceHandler}}
 
        // source1 (mach port)
        CFRunLoopSource {order = 0,  {port = 17923}}
        CFRunLoopSource {order = 0,  {port = 12039}}
        CFRunLoopSource {order = 0,  {port = 16647}}
        CFRunLoopSource {order =-1, {
            callout = PurpleEventCallback}}
        CFRunLoopSource {order = 0, {port = 2407,
            callout = _ZL20notify_port_callbackP12__CFMachPortPvlS1_}}
        CFRunLoopSource {order = 0, {port = 1c03,
            callout = __IOHIDEventSystemClientAvailabilityCallback}}
        CFRunLoopSource {order = 0, {port = 1b03,
            callout = __IOHIDEventSystemClientQueueCallback}}
        CFRunLoopSource {order = 1, {port = 1903,
            callout = __IOMIGMachPortPortCallback}}
 
        // Ovserver
        CFRunLoopObserver {order = -2147483647, activities = 0x1, // Entry
            callout = _wrapRunLoopWithAutoreleasePoolHandler}
        CFRunLoopObserver {order = 0, activities = 0x20,          // BeforeWaiting
            callout = _UIGestureRecognizerUpdateObserver}
        CFRunLoopObserver {order = 1999000, activities = 0xa0,    // BeforeWaiting | Exit
            callout = _afterCACommitHandler}
        CFRunLoopObserver {order = 2000000, activities = 0xa0,    // BeforeWaiting | Exit
            callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}
        CFRunLoopObserver {order = 2147483647, activities = 0xa0, // BeforeWaiting | Exit
            callout = _wrapRunLoopWithAutoreleasePoolHandler}
 
        // Timer
        CFRunLoopTimer {firing = No, interval = 3.1536e+09, tolerance = 0,
            next fire date = 453098071 (-4421.76019 @ 96223387169499),
            callout = _ZN2CAL14timer_callbackEP16__CFRunLoopTimerPv (QuartzCore.framework)}
    },

卡顿优化

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
};
image.png

可以看出,做事情都是在kCFRunLoopBeforeSources后和kCFRunLoopAfterWaiting后,如果长时间停留在这两个状态,则证明主线程发生卡顿

线程保活

线程保活

  1. 子线程需要先获取(创建)runloop并切启动
  2. 往runloop加入事件源或者timer

消息转发流程

objc_msgSend流程分析
objc_msgSend找到imp的流程

1、OC方法经过xcode编译会变成一个objc_msgSend函数,函数是由汇编实现的,进入到汇编代码,
2、我们会发现在objc_msgSend中通过CacheLookup去查找函数的指针,并且在CacheLookup里面会递归从buckets里面查找方法,如果找到指针直接调用CacheHit 触发TailCallCachedImp进行执行方法,如果没找到就触发了CheckMiss,然后走慢速查找流程
3、在Checkmiss里面又调用了__objc_msgSend_uncached函数,在__objc_msgSend_uncached里面调用了MethodTableLookup,
4、在MethodTableLookup里面调用了__class_lookupMethodAndLoadCache3,该函数是由C语言编写的,
5、然后通过lookUpImpOrForward调用了cache_fill_nolock逻辑。

消息转发

当开发者调用了未实现的方法,苹果提供了三个解决途径:
1、resolveInstanceMethod:为发送消息的对象的添加一个IMP,然后再让该对象去处理
2、forwardingTargetForSelector:将该消息转发给能处理该消息的对象
3、methodSignatureForSelector和forwardInvocation:第一个方法生成方法签名,然后创建NSInvocation对象作为参数给第二个方法,
4、然后在第二个方法里面做消息处理,只要在第二个方法里面不执行父类的方法,即使不处理也不会崩溃


image.png

https

证书真伪

image.png

数字签名的制作过程:

CA拥有非对称加密的私钥和公钥。
CA对证书明文信息进行hash。
对hash后的值用私钥加密,得到数字签名。
明文和数字签名共同组成了数字证书,这样一份数字证书就可以颁发给网站了。
那浏览器拿到服务器传来的数字证书后,如何验证它是不是真的?(有没有被篡改、掉包)

浏览器验证过程:

拿到证书,得到明文T,数字签名S。
用CA机构的公钥对S解密(由于是浏览器信任的机构,所以浏览器保有它的公钥。详情见下文),得到S’。
用证书里说明的hash算法对明文T进行hash得到T’。
比较S’是否等于T’,等于则表明证书可信。

关联对象的应用?系统如何实现关联对象的

iOS内存管理-week和关联对象怎么释放
AssociationsManager
AssociationsHashMap
ObjectAssociationMap
ObjcAssociation
线程安全的

image.png

image.png

你可能感兴趣的:(面试2021)