美文网首页2023面试看iOS开发点滴
iOS2023年最新面试题(持续更新中)

iOS2023年最新面试题(持续更新中)

作者: 熊猫丶Panda | 来源:发表于2023-02-12 22:21 被阅读0次

    OC和Swift语言基础

    1、@synthesize和@dynamic分别有什么作用?

    • @property有两个对应的词,一个是 @synthesize,一个是 @dynamic。如果 @synthesize和 @dynamic都没写,那么默认的就是@syntheszie var = _var;
    • @synthesize 的语义是如果你没有手动实现 setter 方法和 getter 方法,那么编译器会自动为你加上这两个方法。
    • @dynamic 告诉编译器:属性的 setter 与 getter 方法由用户自己实现,不自动生成。(当然对于 readonly 的属性只需提供 getter 即可)。假如一个属性被声明为 @dynamic var,然后你没有提供 @setter方法和 @getter 方法,编译的时候没问题,但是当程序运行到 instance.var = someVar,由于缺 setter 方法会导致程序崩溃;或者当运行到 someVar = var 时,由于缺 getter 方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。

    2、Swift和OC的区别?

    • 快速、现代、安全、互动,而且明显优于 Objective-C 语言
    • 可以使用现有的 Cocoa 和 Cocoa Touch 框架
    • Swift 取消了 Objective C 的指针/地址等不安全访问的使用
    • 提供了类似 Java 的名字空间(namespace)、泛型 - (generic)var、运算对象重载(operator overloading
    • Swift 被简单的形容为 “没有 C 的 Objective-C”(Objective-C without the C)
    • 为苹果开发工具带来了Xcode Playgrounds功能,该功能提供强大的互动效果,能让Swift 源代码在撰写过程中实时显示出其运行结果;
    • 基于C和Objective-C,而却没有C的一些兼容约束;
    • 采用了安全的编程模式;
    • 舍弃 Objective C 早期应用 Smalltalk 的语法,保留了Smalltalk的动态特性,全面改为句点表示法
    • 类型严谨 对比oc的动态绑定

    3、Swift中struct和class的区别?

    • struct
      值类型,深拷贝,分配在栈上
      没有析构函数
      不能继承
      不会发生内存泄漏,线程安全
      实例方法修改属性时用mutating标记
    • class
      引用类型,浅拷贝,分配在堆上
      有析构函数
      可以单继承
      可以有单例
      无需mutating标记
      每一个成员变量都必须初始化
    1. class 直接对属性赋值,也就是没有通过构造器赋值的,在创建对象对属性赋值只能是如下方式:
    class ClassPerson  {
        var name: String?
        var age: Int?
    }
    struct StructPerson {
        var name: String?
        var age: Int?
    }
    let p1 = ClassPerson()
    p1.name = "123"
    print(p1.name)
    
    var p2 = StructPerson(name: "abc", age: 20)
    p2.name = "123"
    print(p2.name)
    

    原因: class 在初始化时不能直接把 property 放在默认的 constructor 的参数里,而是需要自己创建一个带参数的constructor

    1. struct是值类型, class是引用类型
    class ClassPerson  {
        var name: String
        var age: Int
        init(name:String,age:Int) {
            self.name = name
            self.age = age
       }
    }
    struct StructPerson {
        var name: String
        var age: Int
    }
    let p1 = ClassPerson(name: "abc", age: 10)
    let secondP1 = p1
    secondP1.name = "123"
    print(p1.name)
    
    let p2 = StructPerson(name: "abc", age: 20)
    var secondP2 = p2
    secondP2.name = "123"
    print(p2.name)
    
    截屏2023-02-14 下午8.52.05.png
    1. 在struct的成员函数中修改自己本身的值,应该在函数签名上加上mutating关键字,而class则没有此限制
    class ClassPerson  {
        var name: String
        var age: Int
        init(name:String,age:Int) {
            self.name = name
            self.age = age
       }
       func changeName(){
            self.name = self.name + "name"
       }
    }
    struct StructPerson {
        var name: String
        var age: Int
        mutating func changeName(){
            self.name = self.name + "name"
        }
    }
    
    let p1 = ClassPerson(name: "abc", age: 10)
    print(p1.name)
    p1.changeName()
    print(p1.name)
    var p2 = StructPerson(name: "abc", age: 20)
    print(p2.name)
    p2.changeName()
    print(p2.name)
    
    截屏2023-02-14 下午8.46.42.png
    1. struct初始化为let的对象无法修改,修改会编译报错,而class没有此限制
    class ClassPerson  {
       var name: String?
       var age: Int?
       init(name:String,age:Int) {
         self.name = name
         self.age = age
       }
    }
    struct StructPerson {
       var name: String?
       var age: Int?
    }
    let p1 = ClassPerson(name: "abc", age: 10)
    p1.name = "123"
    print(p1.name)
    
    let p2 = StructPerson(name: "abc", age: 20)
    p2.name = "123"
    print(p2.name)
    
    截屏2023-02-14 下午8.40.40.png
    1. OC里面无法调用Swift里的struct,因为要在OC里调用Swift代码的话,对象需要继承自NSObject。
    2. struct不能被序列化成NSData,不能归解档,class可以,因为归解档的类必须遵守NSCoding协议,而NSCoding只适用于继承自NSObject的类,struct不能遵守NSCoding协议。
      解决方案:
      定义一个protocol,包含两个方法:
      1.从结构体中得到一个NSDictionary对象
      2.使用一个NSDictionary对象实例化结构体
      NSDictionary可以使用NSKeyedArchiver进行序列化
      好处:所有遵守该协议的结构体都可以被序列化

    4、KVC实现原理?

    KVC,键-值编码,使用字符串直接访问对象的属性。
    底层实现,当一个对象调用setValue方法时,方法内部会做以下操作:

    1. 检查是否存在相应key的set方法,如果存在,就调用set方法
    2. 如果set方法不存在,就会查找与key相同名称并且带下划线的成员属性,如果有,则直接给成员属性赋值
    3. 如果没有找到_key,就会查找相同名称的属性key,如果有就直接赋值
    4. 如果还没找到,则调用valueForUndefinedKey:setValue:forUndefinedKey:方法

    5、KVO的实现原理?

    kvo原理.jpg
    1. 当给A类添加KVO的时候,runtime动态的生成了一个子类NSKVONotifying_A,让A类的isa指针指向NSKVONotifying_A类,重写class方法,隐藏对象真实类信息
    2. 重写监听属性的setter方法,在setter方法内部调用了Foundation 的 _NSSetObjectValueAndNotify函数
    3. _NSSetObjectValueAndNotify函数内部
      a) 首先会调用 willChangeValueForKey
      b) 然后给属性赋值
      c) 最后调用didChangeValueForKey
      d) 最后调用 observer 的observeValueForKeyPath去告诉监听器属性值发生了改变 .
    4. 重写了dealloc做一些 KVO 内存释放

    6、如何手动触发KVO方法?

    • 手动调用willChangeValueForKeydidChangeValueForKey方法
    • 键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey:didChangeValueForKey:。在一个被观察属性发生改变之前, willChangeValueForKey:一定会被调用,这就会记录旧的值。而当改变发生后, didChangeValueForKey :会被调用,继而 observeValueForKey:ofObject:change:context:也会被调用。

    7、为什么Block用copy关键字?

    • Block在没有使用外部变量时,内存存在全局区,然而,当Block在使用外部变量的时候,内存是存在于栈区,当Block copy之后,是存在堆区的。存在于栈区的特点是对象随时有可能被销毁,一旦销毁在调用的时候,就会造成系统的崩溃。所以Block要用copy关键字。

    8、 weak和assign的区别,什么场景下使用,代理为什么使用weak?

    • weak是弱指针, 在对象被销毁的时候会把weak修饰的属性置为空,避免造成野指针,只能修饰对象类型。
    • assign对象被释放的时候不会指向nil,对象被释放了还是指向原来的地址。调用的话容易产生野指针。assign可以修对象和基本数据类型。
    • 代理要使用weak,weak可以说是非持有关系,对象释放了就指向nil,什么时候释放是由外部来控制,可以用assign但是用assign的时需要对象被释放的时候,把delegate指向nil。

    9、load和initialize的区别

    • load方法的本质:直接执行函数指针,其实就是直接执行函数指针,不会执行消息发送objc_msgSend那一套流程。子类、分类的load方法不会覆盖父类的load方法。
    static void schedule_class_load(class_t *cls)
    {
        assert(isRealized(cls));  // _read_images should realize
        if (cls->data->flags & RW_LOADED) return;
        //确保先将父类添加到全局列表里 (loadable_class)
        class_t *supercls = getSuperclass(cls);
        if (supercls) schedule_class_load(supercls);
        //再将当前类添加到全局列表里 (loadable_class)
        add_class_to_loadable_list((Class)cls);
        changeInfo(cls, RW_LOADED, 0); 
    }
    
    // Call all +loads for the detached list.
        for (i = 0; i < used; i++) {
            Class cls = classes[i].cls;
            IMP load_method = classes[i].method;
            if (!cls) continue; 
            if (PrintLoading) {
                _objc_inform("LOAD: +[%s load]\n", _class_getName(cls));
            }
            (*load_method) ((id) cls, SEL_load);
        }
    
    • 场景1 :子类、父类、分类都实现load方法,调用情况
      答:SuperClass->SubClass->CategoryClass

    • 场景2 :子类、父类、分类中子类不实现load方法,调用情况
      答:SuperClass->CategoryClass

    • 场景3 :子类、父类、分类1、分类2都实现load方法,调用情况
      答:SuperClass->SubClass->Category1Class->Category2Class

    • initialize方法的本质
      在类、或者子类,接收到第一条消息之前被执行(如初始化)
      initialize方法最终通过objc_msgSend来执行
      initialize方法在main函数之后调用
      如果一直没有使用类,则initialize方法不会被调用
      如果子类没有实现initialize方法,则会调用父类的initialize方法。

    __private_extern__ void _class_initialize(Class cls)
    {
        Class supercls;
        BOOL reallyInitialize = NO;
    
        // Get the real class from the metaclass. The superclass chain 
        // hangs off the real class only.
        cls = _class_getNonMetaClass(cls);
    
        // Make sure super is done initializing BEFORE beginning to initialize cls.
        // See note about deadlock above.
        supercls = _class_getSuperclass(cls);
        if (supercls  &&  !_class_isInitialized(supercls)) {
            _class_initialize(supercls);
        }
        
        // Try to atomically set CLS_INITIALIZING.
        monitor_enter(&classInitLock);
        if (!_class_isInitialized(cls) && !_class_isInitializing(cls)) {
            _class_setInitializing(cls);
            reallyInitialize = YES;
        }
        monitor_exit(&classInitLock);
        
        if (reallyInitialize) {
            // We successfully set the CLS_INITIALIZING bit. Initialize the class.
            
            // Record that we're initializing this class so we can message it.
            _setThisThreadIsInitializingClass(cls);
            
            // Send the +initialize message.
            // Note that +initialize is sent to the superclass (again) if 
            // this class doesn't implement +initialize. 2157218
            if (PrintInitializing) {
                _objc_inform("INITIALIZE: calling +[%s initialize]",
                             _class_getName(cls));
            }
    
            ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
    
            if (PrintInitializing) {
                _objc_inform("INITIALIZE: finished +[%s initialize]",
                             _class_getName(cls));
            }        
            
            // Done initializing. 
            ......
    }
    

    优先执行父类的initialize方法;通过_class_getSupercass取出父类,递归调用父类的initialize方法;initialize方法最终通过objc_msgSend来执行的。

    • 场景1 :子类、父类都实现initialize方法,调用情况
      答:SuperClass->SubClass
    • 场景2 :子类、父类中子类不实现initialize方法,调用情况
      答:SuperClass->SuperClass(子类未实现,则会调用父类的initialize,导致父类调用多次)
    • 场景3:子类、父类、子类分类都实现initialize方法,调用情况
      答:SuperClass->CategoryClass(category中initialize方法覆盖其本类)
    • 场景4:子类、父类、父类分类1、父类分类2都实现initialize方法,调用情况
      答:CategoryClass->SubClass(category中initialize方法根据Compile Sources排序执行最后一个)

    执行顺序
    load

    load.png
    app启动自动加载所有load方法,load方法会在程序运行前加载一次。
    1.先调用类的load,再调用分类的load
    2.先编译的类,优先调用load,调用子类的load之前,会先调用父类的load
    3.先编译的分类,优先调用load,顺序和Compile Sources中顺序一致

    initialize

    initialize.png
    initialize方法会在类或者子类在 第一次使用的时候调用,当有分类的时候会调用多次,如Son *s = [[Son alloc]init];
    1.父类先于子类执行;(同load方法)
    2.子类未实现,则会调用父类的initialize方法;
    3.分类实现了initialize方法,则会覆盖类中的initialize方法(同category);
    4.存在多个分类,依赖Compile Sources中的顺序,执行最后一个分类的initialize方法(同category);

    使用场景
    1.load通常用于Method Swizzle;
    2.initialize可以用于初始化全局变量或静态变量;initialize方法可能被其分类中的initialize方法覆盖,导致无法调用。
    注意:load和initialize方法内部使用了锁,因此他们是线程安全的。使用时避免阻塞线程,不要使用线程锁。

    10、如何理解copy-on-write?

    苹果建议当复制大的值类型数据的时候,使用写时复制技术,那什么是写时复制呢?我们现在看一段代码:
    
    值类型(比如:struct),在复制时,复制对象与原对象实际上在内存中指向同一个对象,当且仅当修改复制的对象时,才会在内存中创建一个新的对象
    为了提升性能,Struct, String、Array、Dictionary、Set采取了Copy On Write的技术
    
    比如仅当有“写”操作时,才会真正执行拷贝操作
    
    对于标准库值类型的赋值操作,Swift 能确保最佳性能,所有没必要为了保证最佳性能来避免赋值
    var array1: [Int] = [0, 1, 2, 3]
    var array2 = array1
    
    print(address: array1) //0x600000078de0
    print(address: array2) //0x600000078de0
    
    array2.append(4)
    
    print(address: array2) //0x6000000aa100
    
    我们看到当array2的值没有发生变化的时候,array1和array2指向同一个地址,但是当array2的发生变化时,array2指向地址也变了,很奇怪是吧。
    

    UI

    1、UIView和CALayer的区别和联系?

    • UIView 继承 UIResponder,而 UIResponder 是响应者对象,可以对iOS 中的事件响应及传递,CALayer 没有继承自 UIResponder,所以 CALayer 不具备响应处理事件的能力。CALayer 是 QuartzCore 中的类,是一个比较底层的用来绘制内容的类,用来绘制UI
    • UIView 对 CALayer 封装属性,对 UIView 设置 frame、center、bounds 等位置信息时,其实都是UIView 对 CALayer 进一层封装,使得我们可以很方便地设置控件的位置;例如圆角、阴影等属性, UIView 就没有进一步封装,所以我们还是需要去设置 Layer 的属性来实现功能。
    • UIView 是 CALayer 的代理,UIView 持有一个 CALayer 的属性,并且是该属性的代理,用来提供一些 CALayer 行的数据,例如动画和绘制。

    2、谈谈对UIResponder的理解?

    UIResponder类是专门用来响应用户的操作处理各种事件的,包括触摸事件(Touch Events)、运动事件(Motion Events)、远程控制事件(Remote Control Events)。我们知道UIApplication、UIView、UIViewController这几个类是直接继承自UIResponder,所以这些类都可以响应事件。当然我们自定义的继承自UIView的View以及自定义的继承自UIViewController的控制器都可以响应事件。

    • 响应过程
      iOS系统检测到手指触摸(Touch)操作时会将其打包成一个UIEvent对象,并放入当前活动Application的事件队列,单例的UIApplication会从事件队列中取出触摸事件并传递给单例的UIWindow来处理,UIWindow对象首先会使用hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图(最合适来处理的控件),这个过程称之为hit-test view。
      那么什么是最适合来处理事件的控件?
      1.自己能响应触摸事件
      2.触摸点在自己身上
      3.从后往前递归遍历子控件, 重复上两步
      4.如果没有符合条件的子控件, 那么就自己最合适处理
    1. hitTest:withEvent:事件传递给控件的时候, 就会调用该方法,去寻找最合适的view并返回看可以响应的view
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
        // 1.如果控件不允许与用用户交互,那么返回nil
        if (self.userInteractionEnabled == NO || self.alpha <= 0.01 || self.hidden == YES){
            return nil;
        }
        // 2\. 如果点击的点在不在当前控件中,返回nil
        if (![self pointInside:point withEvent:event]){
            return nil;
        }
        // 3.从后往前遍历每一个子控件
        for(int i = (int)self.subviews.count - 1 ; i >= 0 ;i--){
            // 3.1获取一个子控件
            UIView *childView = self.subviews[i];
            // 3.2当前触摸点的坐标转换为相对于子控件触摸点的坐标
            CGPoint childP = [self convertPoint:point toView:childView];
            // 3.3判断是否在在子控件中找到了更合适的子控件(递归循环)
            UIView *fitView = [childView hitTest:childP withEvent:event];
            // 3.4如果找到了就返回
            if (fitView) {
                return fitView;
            }
        }
        // 4.没找到,表示没有比自己更合适的view,返回自己
        return self;
    }
    
    1. pointInside:withEvent:该方法判断触摸点是否在控件身上,是则返回YES,否则返回NO,point参数必须是方法调用者的坐标系.
    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
        return NO;
    }
    
    

    3、loadView的作用?

    loadView方法会在每次访问UIViewController的view(比如controller.view、self.view)而且view为nil时会被调用,此方法主要用来负责创建UIViewController的view(重写loadView方法,并且不需要调用[super loadView])
    [super loadView]执行流程:

    • 它会先去查找与UIViewController相关联的xib文件,通过加载xib文件来创建UIViewController的view,如果在初始化UIViewController指定了xib文件名,就会根据传入的xib文件名加载对应的xib文件,如果没有明显地传xib文件名,就会加载跟UIViewController同名的xib文件
    • 如果没有找到相关联的xib文件,就会创建一个空白的UIView,然后赋值给UIViewController的view属性
    • 综上,在需要自定义UIViewController的view时,可以通过重写loadView方法且不需要调用[super loadView]方法。

    内存管理

    RunLoop

    1、RunLoop 的本质是什么?

    “Run loops are part of thefundamental infrastructure associated withthreads. A run loop is an event processing loopthat you use to schedule work and coordinatethe receipt of incoming events. The purpose ofa runloop is to keep your thread busy whenthere is work to do and put your thread tosleep when there is none.”

    typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;
    
    struct __CFRunLoop {
        CFRuntimeBase _base;
        pthread_mutex_t _lock;          /* locked for accessing mode list */
        //mach_port
        __CFPort _wakeUpPort;           // used for CFRunLoopWakeUp 
        Boolean _unused;
        volatile _per_run_data *_perRunData;              // reset for runs of the run loop
        pthread_t _pthread;
        uint32_t _winthread;
        CFMutableSetRef _commonModes;
        CFMutableSetRef _commonModeItems;
        CFRunLoopModeRef _currentMode;
        CFMutableSetRef _modes;
        struct _block_item *_blocks_head;
        struct _block_item *_blocks_tail;
        CFAbsoluteTime _runTime;
        CFAbsoluteTime _sleepTime;
        CFTypeRef _counterpart;
    };
    

    1.NSRunLoop只是比CFRunLoop多了一层简单的OC封装,底层还是CFRunLoop,CFRunLoop本质是一个结构体,而NSRunLoop是一个NSObject对象。NSRunLoop存在于Foundation框架中,CFRunLoop是存在于CoreFoundation框架中的。NSRunLoop不是线程安全的,CFRunLoop时候线程安全的。

    2.RunLoop是一个与线程相关的底层机制,用来接收事件和调度任务。runloop目的是让线程在有工作的时候保持忙碌,在没有工作的时候睡眠。

    3.RunLoop是与线程相关的,它们的关系一一对应:一个线程只能对应一个RunLoop,即在某一时刻,一个线程只能运行在某一个RunLoop上。当运行一个应用程序的时候,系统会为应用程序的主线程创建一个RunLoop用来处理主线程上的事件,例如UI刷新和触屏事件。因此,开发者不需要为主线程显式地创建和运行一个RunLoop,而子线程需要显式地运行一个RunLoop,再将辅助线程放到RunLoop中运行,否则线程不会自动开启RunLoop。

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

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

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

    • Mode,运行模式
    struct __CFRunLoopMode {
        CFRuntimeBase _base;
        pthread_mutex_t _lock;  /* must have the run loop locked before locking this */
        CFStringRef _name;     // mode名称
        Boolean _stopped;      // mode是否被终止
        char _padding[3];
        // 几种事件,下面这四个字段,在苹果官方文档里面称为Item
        // RunLoop中有个commomitems字段,里面就是保存的下面这些内容
        CFMutableSetRef _sources0;
        CFMutableSetRef _sources1;
        CFMutableArrayRef _observers;
        CFMutableArrayRef _timers;
        CFMutableDictionaryRef _portToV1SourceMap;   //字典  key是mach_port_t,value是CFRunLoopSourceRef
        __CFPortSet _portSet;    //保存所有需要监听的port,比如_wakeUpPort,_timerPort都保存在这个数组中
        CFIndex _observerMask;
    #if USE_DISPATCH_SOURCE_FOR_TIMERS
        dispatch_source_t _timerSource;
        dispatch_queue_t _queue;
        Boolean _timerFired; // set to true by the source when a timer has fired
        Boolean _dispatchTimerArmed;
    #endif
    #if USE_MK_TIMER_TOO
        mach_port_t _timerPort;
        Boolean _mkTimerArmed;
    #endif
    #if DEPLOYMENT_TARGET_WINDOWS
        DWORD _msgQMask;
        void (*_msgPump)(void);
    #endif
        uint64_t _timerSoftDeadline; /* TSR */
        uint64_t _timerHardDeadline; /* TSR */
    };
    
    
    1. 模式(Mode)指的是一个包括输入源(Inputsource)、定时器(Timer)、观察者(Observer)的模型对象。简单点来说,模式就是用来存储runloop需要响应的事件,这些事件包括许多输入源、定时器和观察者。

    2. 系统默认注册5个Mode:
      1.NSDefaultRunLoopMode: App的默认Mode,通常主线程是在这个Mode下运行
      2.UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView`追踪触摸滑动,保证界面滑动时不受其他Mode影响。
      3.NSRunLoopCommonModes:并不是一个真的模式,它只是一个标记,如:被标记的 Timer可以在Default模式和UITracking下运行。
      4.UIInitializationRunLoopMode:私有的mode,App启动的时候的状态,加载出第一个页面后,就转成了Default,通常用不到
      5.GSEventReceiveRunLoopMode:系统的内部 Mode,通常用不到

    • Source,输入源/事件源
    //CFRunLoop.h
    typedef struct __CFRunLoopSource *CFRunLoopSourceRef;
    //CFRunLoop.c
    struct __CFRunLoopSource{
        CFRuntimeBase _base;//
        uint32_t _bits;
        pthread_mutex_t lock;
        CFIndex _order;  /*immutable*/
        CFMutableBagRef _runLoops;
        union{
            CFRunLoopSourceContext version0; /*immutable,except invalidation*/
            CFRunLoopSourceContext1 version1; /*immutable,except invalidattion*/
        }_context;
    };
    

    1.联合体的作用是共享存储空间,也就是说,version0和version1两个变量共享一段存储空间,一个__CFRunLoopSource结构体变量要么对应version0类型的事件源,要么对应version1类型的事件源。其中,version0和version1分别在源码中对应事件源Source0和Source1。

    2.Source0对应需要手动触发的事件,对应官方文档Input Source中的Custom和performSelector:onThread事件源。

    3.Source1表示基于端口触发的事件,对应官方文档Input Source中Port的事件源。

    • Timer,定时源
    struct __CFRunLoopTimer {
        CFRuntimeBase _base;
        uint16_t _bits;  //标记fire状态
        pthread_mutex_t _lock;
        CFRunLoopRef _runLoop;        //添加该timer的runloop
        CFMutableSetRef _rlModes;     //存放所有 包含该timer的 mode的 modeName,意味着一个timer可能会在多个mode中存在
        CFAbsoluteTime _nextFireDate;
        CFTimeInterval _interval;     //理想时间间隔  /* immutable */
        CFTimeInterval _tolerance;    //时间偏差      /* mutable */
        uint64_t _fireTSR;          /* TSR units */
        CFIndex _order;         /* immutable */
        CFRunLoopTimerCallBack _callout;    /* immutable */
        CFRunLoopTimerContext _context; /* immutable, except invalidation */
    };
    
    

    __CFRunLoopTimer是一个基于mk_timer实现的定时器,通过_callout回调实现定时执行任务。NSTimer其实是对CFRunLoopTimerRef的一个上层封装。

    • Observer,观察者
    struct __CFRunLoopObserver {
        CFRuntimeBase _base;
        pthread_mutex_t _lock;
        CFRunLoopRef _runLoop;   //添加该Observer的RunLoop
        CFIndex _rlCount;
        CFOptionFlags _activities;      /* immutable */
        CFIndex _order;         /* immutable */
        CFRunLoopObserverCallBack _callout;     //设置回调函数,回调指针  /* immutable */
        CFRunLoopObserverContext _context;  /* immutable, except invalidation */
    };
    

    CFRunLoopObserver是观察者,可以观察RunLoop的各种状态,每个 Observer 都包含了一个回调(也就是上面的CFRunLoopObserverCallBack函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。

    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         // 所有状态
    };
    

    1)kCFRunLoopEntry表示刚进入runloop的时候。
    2)kCFRunLoopBeforeTimers表示将要处理timer。
    3)kCFRunLoopBeforeSources表示将要处理Source。
    4)kCFRunLoopBeforeWaiting表示将要进入休眠状态。
    5)kCFRunLoopAfterWaiting表示将要从休眠状态进入唤醒状态。
    6)kCFRunLoopExit表示退出状态。
    7)kCFRunLoopAllActivities表示所有1)~6)中的状态。

    5、Runloop 的工作流程?

    Runloop 的工作流程.png

    内部逻辑:

    1. 通知 Observer 已经进入了 RunLoop
    2. 通知 Observer 即将处理 Timer
    3. 通知 Observer 即将处理非基于端口的输入源(即将处理 Source0)
    4. 处理那些准备好的非基于端口的输入源(处理 Source0)
    5. 如果基于端口的输入源准备就绪并等待处理,请立刻处理该事件。转到第 9 步(处理 Source1)
    6. 通知 Observer 线程即将休眠
    7. 将线程置于休眠状态,直到发生以下事件之一
    • 事件到达基于端口的输入源(port-based input sources)(也就是 Source0)
    • Timer 到时间执行
    • 外部手动唤醒
    • 为 RunLoop 设定的时间超时
    1. 通知 Observer 线程刚被唤醒(还没处理事件)
    2. 处理待处理事件
    • 如果是 Timer 事件,处理 Timer 并重新启动循环,跳到第 2 步
    • 如果输入源被触发,处理该事件(文档上是 deliver the event)
    • 如果 RunLoop 被手动唤醒但尚未超时,重新启动循环,跳到第 2 步

    6、Runloop 有哪些应用?

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

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

    • 用户态->内核态 没有消息需要处理时,休眠以避免资源占用;
    • 内核态->用户态 有消息需要处理时,立刻被唤醒。

    8、点击APP图标,从程序启动、运行、退出这个过程当中,系统都发生了什么?

    • 程序启动后,调用main函数后,会调用UIApplicationmain函数,此函数内部会启动主线程的RunLoop,经过一系列处理,最终主线程RunLoop处于休眠状态;
    • 如果此时点击了屏幕,会产生一个mach_port,基于mach_port最终转成Source1,唤醒主线程,运行处理;
    • 当把程序杀死后,RunLoop退出,并且发送通知给观察者。RunLoop退出后线程即刻销毁。

    Runtime

    1、概念

    • oc是一门动态语言,所谓动态语言就是在编译阶段无法确定调用的函数以及属性的类型,只有在运行阶段首次确定类型和调用的函数。
    • Runtime就是动态语言下核心的一个库,底层都会通过objc_msgSend来处理消息转发机制。也是因为拥有Runtime使得oc语言灵活性比较强,能够具有动态、动态绑定、动态解析的特性。
    1. objc_msgSend
    /* Basic Messaging Primitives
    *
    * On some architectures, use objc_msgSend_stret for some struct return types.
    * On some architectures, use objc_msgSend_fpret for some float return types.
    * On some architectures, use objc_msgSend_fp2ret for some float return types.
    *
    * These functions must be cast to an appropriate function pointer type 
    * before being called. 
    */
    

    这是官方的声明,这是个最基本的用于发送消息的函数。另外,这个函数并不能发送所有类型的消息,只能发送基本的消息。比如,在一些处理器上,我们必须使用objc_msgSend_stret来发送返回值类型为结构体的消息,使用objc_msgSend_fpret来发送返回值类型为浮点类型的消息,而又在一些处理器上,还得使用objc_msgSend_fp2ret来发送返回值类型为浮点类型的消息。要调用objc_msgSend函数,必须要将函数强制转换成合适的函数指针类型才能调用。
    objc_msgSend函数的声明来看,它应该是不带返回值的,但是我们在使用中却可以强制转换类型,以便接收返回值。另外,它的参数列表是可以任意多个的,前提也是要强制函数指针类型。
    编译器会根据情况在objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, 或 objc_msgSendSuper_stret四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有”Super”的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有”stret”的函数。

    1. id

    objc_msgSend第一个参数类型为id,它是一个指向objc_object结构体的指针:

    typedef struct objc_object *id;
    
    struct objc_object {
    private:
        isa_t isa;
    
    public:
    
        // ISA() assumes this is NOT a tagged pointer object
        Class ISA();
    
        // rawISA() assumes this is NOT a tagged pointer object or a non pointer ISA
        Class rawISA();
    
        // getIsa() allows this to be a tagged pointer object
        Class getIsa();
        
        uintptr_t isaBits() const;
    
        // initIsa() should be used to init the isa of new objects only.
        // If this object already has an isa, use changeIsa() for correctness.
        // initInstanceIsa(): objects with no custom RR/AWZ
        // initClassIsa(): class objects
        // initProtocolIsa(): protocol objects
        // initIsa(): other objects
        void initIsa(Class cls /*nonpointer=false*/);
        void initClassIsa(Class cls /*nonpointer=maybe*/);
        void initProtocolIsa(Class cls /*nonpointer=maybe*/);
        void initInstanceIsa(Class cls, bool hasCxxDtor);
    
        // changeIsa() should be used to change the isa of existing objects.
        // If this is a new object, use initIsa() for performance.
        Class changeIsa(Class newCls);
    
        bool hasNonpointerIsa();
        bool isTaggedPointer();
        bool isBasicTaggedPointer();
        bool isExtTaggedPointer();
        bool isClass();
    
        // object may have associated objects?
        bool hasAssociatedObjects();
        void setHasAssociatedObjects();
    
        // object may be weakly referenced?
        bool isWeaklyReferenced();
        void setWeaklyReferenced_nolock();
    
        // object may have -.cxx_destruct implementation?
        bool hasCxxDtor();
    
        // Optimized calls to retain/release methods
        id retain();
        void release();
        id autorelease();
    
        // Implementations of retain/release methods
        id rootRetain();
        bool rootRelease();
        id rootAutorelease();
        bool rootTryRetain();
        bool rootReleaseShouldDealloc();
        uintptr_t rootRetainCount();
    
        // Implementation of dealloc methods
        bool rootIsDeallocating();
        void clearDeallocating();
        void rootDealloc();
    
    private:
        void initIsa(Class newCls, bool nonpointer, bool hasCxxDtor);
    
        // Slow paths for inline control
        id rootAutorelease2();
        uintptr_t overrelease_error();
    
    #if SUPPORT_NONPOINTER_ISA
        // Unified retain count manipulation for nonpointer isa
        id rootRetain(bool tryRetain, bool handleOverflow);
        bool rootRelease(bool performDealloc, bool handleUnderflow);
        id rootRetain_overflow(bool tryRetain);
        uintptr_t rootRelease_underflow(bool performDealloc);
    
        void clearDeallocating_slow();
    
        // Side table retain count overflow for nonpointer isa
        void sidetable_lock();
        void sidetable_unlock();
    
        void sidetable_moveExtraRC_nolock(size_t extra_rc, bool isDeallocating, bool weaklyReferenced);
        bool sidetable_addExtraRC_nolock(size_t delta_rc);
        size_t sidetable_subExtraRC_nolock(size_t delta_rc);
        size_t sidetable_getExtraRC_nolock();
    #endif
    
        // Side-table-only retain count
        bool sidetable_isDeallocating();
        void sidetable_clearDeallocating();
    
        bool sidetable_isWeaklyReferenced();
        void sidetable_setWeaklyReferenced_nolock();
    
        id sidetable_retain();
        id sidetable_retain_slow(SideTable& table);
    
        uintptr_t sidetable_release(bool performDealloc = true);
        uintptr_t sidetable_release_slow(SideTable& table, bool performDealloc = true);
    
        bool sidetable_tryRetain();
    
        uintptr_t sidetable_retainCount();
    #if DEBUG
        bool sidetable_present();
    #endif
    };
    

    objc_object结构体包含一个isa指针,根据isa指针就可以找到对象所属的类。
    注意:isa指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用class方法来确定实例对象的类。

    union isa_t {
        isa_t() { }
        isa_t(uintptr_t value) : bits(value) { }
    
        Class cls;
        uintptr_t bits;
    #if defined(ISA_BITFIELD)
        struct {
            ISA_BITFIELD;  // defined in isa.h
        };
    #endif
    };
    
    # if __arm64__
    // ARM64 simulators have a larger address space, so use the ARM64e
    // scheme even when simulators build for ARM64-not-e.
    #   if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
    #     define ISA_MASK        0x007ffffffffffff8ULL
    #     define ISA_MAGIC_MASK  0x0000000000000001ULL
    #     define ISA_MAGIC_VALUE 0x0000000000000001ULL
    #     define ISA_HAS_CXX_DTOR_BIT 0
    #     define ISA_BITFIELD                                                      \
            uintptr_t nonpointer        : 1;                                       \
            uintptr_t has_assoc         : 1;                                       \
            uintptr_t weakly_referenced : 1;                                       \
            uintptr_t shiftcls_and_sig  : 52;                                      \
            uintptr_t has_sidetable_rc  : 1;                                       \
            uintptr_t extra_rc          : 8
    #     define ISA_HAS_INLINE_RC    1
    #     define RC_HAS_SIDETABLE_BIT 55
    #     define RC_ONE_BIT           (RC_HAS_SIDETABLE_BIT+1)
    #     define RC_ONE               (1ULL<<RC_ONE_BIT)
    #     define RC_HALF              (1ULL<<7)
    #   else
    #     define ISA_MASK        0x0000000ffffffff8ULL
    #     define ISA_MAGIC_MASK  0x000003f000000001ULL
    #     define ISA_MAGIC_VALUE 0x000001a000000001ULL
    #     define ISA_HAS_CXX_DTOR_BIT 1
    #     define ISA_BITFIELD                                                      \
            uintptr_t nonpointer        : 1;                                       \
            uintptr_t has_assoc         : 1;                                       \
            uintptr_t has_cxx_dtor      : 1;                                       \
            uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
            uintptr_t magic             : 6;                                       \
            uintptr_t weakly_referenced : 1;                                       \
            uintptr_t unused            : 1;                                       \
            uintptr_t has_sidetable_rc  : 1;                                       \
            uintptr_t extra_rc          : 19
    #     define ISA_HAS_INLINE_RC    1
    #     define RC_HAS_SIDETABLE_BIT 44
    #     define RC_ONE_BIT           (RC_HAS_SIDETABLE_BIT+1)
    #     define RC_ONE               (1ULL<<RC_ONE_BIT)
    #     define RC_HALF              (1ULL<<18)
    #   endif
    

    所以在isa_t联合体中Class cls和uintptr_t bits是互斥的。
    由 typedef unsigned long uintptr_t; 所知,bits占据8字节,共64位,64位中存储的即ISA_BITFIELD宏定义中的内容。

    uintptr_t nonpointer : 1; 是否对isa指针开启优化。0:纯isa指针 1:不只类对象地址,还包括了类信息,对象对引用计数等。
    uintptr_t has_assoc : 1; 关联对象标识位 0:没有 1:存在。
    uintptr_t has_cxx_dtor : 1; 是否有c++或objc的析构函数 如果有则需要调用析构逻辑,如果没有则可以更快释放对象。
    uintptr_t shiftcls : 33; 存储类指针的值,开启指针优化时,有33位用来存放类指针。
    uintptr_t magic : 6; 用于调试器判断当前对象是真的对象还是未初始化的空间。
    uintptr_t weakly_referenced : 1; 标志对象是否被指向或曾经指向一个ARC的弱变量,没有弱引用的对象可以更快的释放。
    uintptr_t deallocating : 1; 标志对象是否正在释放内存。
    uintptr_t has_sidetable_rc : 1; 当引用计数大于10时,则需要借助该变量存储进位。
    uintptr_t extra_rc : 19 表示该对象的引用计数减1,如果引用计数为10,则extra_rc为9,如果引用计数大于10,则需要借助has_sidetable_rc。

    1. SEL
      SEL其实是一个指向objc_selector结构体的指针:typedef struct objc_selector *SEL;
      objc_msgSend函数第二个参数类型为SEL,它是selector在Objc中的表示类型(Swift中是Selector类)。
      其实它就是个映射到方法的C字符串,你可以用 Objc 编译器命令@selector()或者 Runtime 系统的sel_registerName函数来获得一个SEL类型的方法选择器。

    2. Class

    Class其实是一个指向objc_class结构体的指针:
    typedef struct objc_class *Class;

    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();
        }
        void setData(class_rw_t *newData) {
            bits.setData(newData);
        }
        const class_ro_t *safe_ro() const {
            return bits.safe_ro();
        }
        .....
    }
    
    struct class_data_bits_t {
        friend objc_class;
        class_rw_t* data() const {
          ...
        }
        void setData(class_rw_t *newData)
        {
          ...
        }
    
        const class_ro_t *safe_ro() const {
          ...
        }
    }
    
    struct cache_t {
        explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
        // _bucketsAndMaybeMask is a buckets_t pointer in the top 28 bits
        union {
            // Note: _flags on ARM64 needs to line up with the unused bits of
            // _originalPreoptCache because we access some flags (specifically
            // FAST_CACHE_HAS_DEFAULT_CORE and FAST_CACHE_HAS_DEFAULT_AWZ) on
            // unrealized classes with the assumption that they will start out
            // as 0.
            struct {
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED && !__LP64__
                // Outlined cache mask storage, 32-bit, we have mask and occupied.
                explicit_atomic<mask_t>    _mask;
                uint16_t                   _occupied;
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED && __LP64__
                // Outlined cache mask storage, 64-bit, we have mask, occupied, flags.
                explicit_atomic<mask_t>    _mask;
                uint16_t                   _occupied;
                uint16_t                   _flags;
    #   define CACHE_T_HAS_FLAGS 1
    #elif __LP64__
                // Inline cache mask storage, 64-bit, we have occupied, flags, and
                // empty space to line up flags with originalPreoptCache.
                //
                // Note: the assembly code for objc_release_xN knows about the
                // location of _flags and the
                // FAST_CACHE_HAS_CUSTOM_DEALLOC_INITIATION flag within. Any changes
                // must be applied there as well.
                uint32_t                   _unused;
                uint16_t                   _occupied;
                uint16_t                   _flags;
    #   define CACHE_T_HAS_FLAGS 1
    #else
                // Inline cache mask storage, 32-bit, we have occupied, flags.
                uint16_t                   _occupied;
                uint16_t                   _flags;
    #   define CACHE_T_HAS_FLAGS 1
    #endif
    
            };
            explicit_atomic<preopt_cache_t *> _originalPreoptCache;
        };
        ...
    public:
        // The following four fields are public for objcdt's use only.
        // objcdt reaches into fields while the process is suspended
        // hence doesn't care for locks and pesky little details like this
        // and can safely use these.
        unsigned capacity() const;
        struct bucket_t *buckets() const;
        Class cls() const;
    };
    
    struct bucket_t {
    private:
        // IMP-first is better for arm64e ptrauth and no worse for arm64.
        // SEL-first is better for armv7* and i386 and x86_64.
    #if __arm64__
        explicit_atomic<uintptr_t> _imp;
        explicit_atomic<SEL> _sel;
    #else
        explicit_atomic<SEL> _sel;
        explicit_atomic<uintptr_t> _imp;
    #endif
    }
    
    struct class_rw_t {
        ...
        explicit_atomic<uintptr_t> ro_or_rw_ext;
        ...
    private:
        using ro_or_rw_ext_t = objc::PointerUnion<const class_ro_t, class_rw_ext_t, PTRAUTH_STR("class_ro_t"), PTRAUTH_STR("class_rw_ext_t")>;
        const ro_or_rw_ext_t get_ro_or_rwe() const {
            return ro_or_rw_ext_t{ro_or_rw_ext};
        }
    
        // 设置ro
        const class_ro_t *ro() const {
            auto v = get_ro_or_rwe();
            if (slowpath(v.is<class_rw_ext_t *>())) {
                return v.get<class_rw_ext_t *>()->ro;
            }
            return v.get<const class_ro_t *>();
        }
        void set_ro(const class_ro_t *ro) {
            auto v = get_ro_or_rwe();
            if (v.is<class_rw_ext_t *>()) {
                v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro = ro;
            } else {
                set_ro_or_rwe(ro);
            }
        }
    
        // 获取相关信息
        const method_array_t methods() const {
            auto v = get_ro_or_rwe();
            if (v.is<class_rw_ext_t *>()) {
                return v.get<class_rw_ext_t *>()->methods;
            } else {
                return method_array_t{v.get<const class_ro_t *>()->baseMethods()};
            }
        }
        const property_array_t properties() const {
            auto v = get_ro_or_rwe();
            if (v.is<class_rw_ext_t *>()) {
                return v.get<class_rw_ext_t *>()->properties;
            } else {
                return property_array_t{v.get<const class_ro_t *>()->baseProperties};
            }
        }
        const protocol_array_t protocols() const {
            auto v = get_ro_or_rwe();
            if (v.is<class_rw_ext_t *>()) {
                return v.get<class_rw_ext_t *>()->protocols;
            } else {
                return protocol_array_t{v.get<const class_ro_t *>()->baseProtocols};
            }
        }
    }
    
    struct class_rw_ext_t {
        const class_ro_t *ro;
        method_array_t methods;
        property_array_t properties;
        protocol_array_t protocols;
        char *demangledName;
        uint32_t version;
    };
    
    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;
        // This field exists only when RO_HAS_SWIFT_INITIALIZER is set.
        _objc_swiftMetadataInitializer __ptrauth_objc_method_list_imp _swiftMetadataInitializer_NEVER_USE[0];
        _objc_swiftMetadataInitializer swiftMetadataInitializer() const {
            if (flags & RO_HAS_SWIFT_INITIALIZER) {
                return _swiftMetadataInitializer_NEVER_USE[0];
            } else {
                return nil;
            }
        }
        method_list_t *baseMethods() const {
            return baseMethodList;
        }
        class_ro_t *duplicate() const {
            if (flags & RO_HAS_SWIFT_INITIALIZER) {
                size_t size = sizeof(*this) + sizeof(_swiftMetadataInitializer_NEVER_USE[0]);
                class_ro_t *ro = (class_ro_t *)memdup(this, size);
                ro->_swiftMetadataInitializer_NEVER_USE[0] = this->_swiftMetadataInitializer_NEVER_USE[0];
                return ro;
            } else {
                size_t size = sizeof(*this);
                class_ro_t *ro = (class_ro_t *)memdup(this, size);
                return ro;
            }
        }
    };
    

    Class本身是运行时加载的,在运行时会被改变,所以本身Class就是属于脏内存。那么如果想要获取Class的干净内存,也就是编译时确定的数据结构包括方法列表、成员变量等的,该怎么办?这其实就是class_ro_t的作用。因为class_ro_t是只读,意味着class_ro_t是从mach-o读取类的数据之后,就不会被改变。那如果我们想在运行时修改类的信息,比如添加方法,比如加载category怎么办呢?那这时候就有一个与之对应的class_rw_t结构,class_rw_t是运行时存储类的信息,可读可写的,可以在运行时修改。说到这里,好像还漏掉一个结构class_rw_ext_t,这个东西又是干什么用的呢?存在的意义是什么?其实还是跟运行时有关。实际上在我们的app运行中,需要运行时修改的类是非少的,据统计平均大概就10%左右。那也就是说大部分只需要读取class_ro_t中的数据就够了,少部分才需要修改。因此才会有class_rw_ext_t这个扩展的结构体。class_rw_ext_t的作用是这样的:当我们需要修改类结构时,比如添加方法、加载category等时,class_rw_t回去开辟一个额外的空间rwe(class_rw_ext_t),用于存储新的方法和class_ro_t中的方法等信息。这样做的目的有一个好处就是,对于绝大部分类是不需要这个开辟class_rw_ext_t这个结构体,节省内存。

    2、Runtime 如何实现 weak 属性?

    weak 此特质表明该属性定义了一种「非拥有关系」(nonowning relationship)。为这种属性设置新值时,设置方法既不持有新值(新指向的对象),也不释放旧值(原来指向的对象)。

    Runtime 对注册的类,会进行内存布局,维护一个 hash 表,这是一个全局表,表中是用 weak 指向的对象内存地址作为 key,用所有指向该对象的weak指针表作为 value。当此对象的引用计数为 0 的时候会调用dealloc,假如该对象内存地址是 a,那么就会以 a 为 key,在这个 weak 表中搜索,找到所有以 a 为键的 weak 对象,从而设置为 nil。

    Runtime 如何实现 weak 属性具体流程大致分为 3 步:

    1. 初始化时:runtime 会调用objc_initWeak()函数,初始化一个新的 weak 指针指向对象的地址。
    2. 添加引用时:objc_initWeak()函数会调用objc_storeWeak()函数,objc_storeWeak()的作用是更新指针指向(指针可能原来指向着其他对象,这时候需要将该 weak 指针与旧对象解除绑定,会调用到weak_unregister_no_lock,如果指针指向的新对象非空,则创建对应的弱引用表,将 weak 指针与新对象进行绑定,会调用到weak_register_no_lock。在这个过程中,为了防止多线程中竞争冲突,会有一些锁的操作。
    3. 释放时:调用clearDeallocating()函数,该函数首先根据对象地址获取所有 weak 指针地址的数组,然后遍历这个数组把其中的数据设为 nil,最后把这个 entry 从 weak 表中删除,最后清理对象的记录。

    3、Runtime具体应用

    1. 利用关联对象(AssociatedObject)给分类添加属性
    objc_setAssociatedObject(<#id object#>, <#const void *key#>, <#id value#>, <#objc_AssociationPolicy policy#>)
    
    • 第一个参数: id object : 需要传入的是 : 对象的主分支
    • 第二个参数: const void *key : 是一个 static 类型的关键字,这里根据开发者自身来定义就行
    • 第三个参数: id value : 传入的是: 对象的子分支
    • 第四个参数: objc_AssociationPolicy policy :是当前关联对象的类型 strong,weak,copy (枚举类型:开发者可以点进去看)
    objc_getAssociatedObject(<#id object#>, <#const void *key#>)就相对来说容易理解一点了
    
    • 第一个参数 : id object : 需要传入的是 : 对象的主分支
    • 第二个参数 : const void *key : 是一个 static 类型的关键字,这里根据开发者自身来定义就行
    1. 遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)
    2. 交换方法实现,在方法上增加额外功能(交换系统的方法)
    • 有这样一个场景,出于某些需求,我们需要跟踪记录APP中按钮的点击次数和频率等数据,怎么解决?当然通过继承按钮类或者通过类别实现是一个办法,但是带来其他问题比如别人不一定会去实例化你写的子类,或者其他类别也实现了点击方法导致不确定会调用哪一个,runtime可以这样解决:
    @implementation UIButton (Hook)
    
    + (void)load {
    
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
    
            Class selfClass = [self class];
    
            SEL oriSEL = @selector(sendAction:to:forEvent:);
            Method oriMethod = class_getInstanceMethod(selfClass, oriSEL);
    
            SEL cusSEL = @selector(mySendAction:to:forEvent:);
            Method cusMethod = class_getInstanceMethod(selfClass, cusSEL);
    
            BOOL addSucc = class_addMethod(selfClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
            if (addSucc) {
                class_replaceMethod(selfClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
            } else {
                method_exchangeImplementations(oriMethod, cusMethod);
            }
    
        });
    }
    
    - (void)mySendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
        [CountTool addClickCount];
        [self mySendAction:action to:target forEvent:event];
    }
    
    @end
    
    1. 利用消息转发机制解决方法找不到的异常问题
    2. 动态变量控制
      在程序中,xiaowen的age是20,后来被runtime变成10
    • 动态获取xiaowen类中的所有属性[包括私有]
      Ivar *ivar = class_copyIvarList([self.xiaowen class], &count);

    • 遍历属性找到对应name字段
      const char *varName = ivar_getName(var);

    • 修改对应的字段值成20
      object_setIvar(self.xiaowen, var, @"20");

    - (void)changeAge {
         unsigned int count = 0;
         Ivar *ivar = class_copyIvarList([self.xiaowen class], &count);
         for (int i = 0; i<count; i++) {
             Ivar var = ivar[i];
             const char *varName = ivar_getName(var);
             NSString *name = [NSString stringWithUTF8String:varName];
             if ([name isEqualToString:@"_age"]) {
                 object_setIvar(self.xiaowen, var, @"20");
                 break;
             }
         }
         NSLog(@"age is %@",self.xiaowen.age);
     }
    
    1. 动态添加方法
    • 动态给Person类中添加study方法:
     - (void)addMethod {
         class_addMethod([self.xiaowen class], @selector(study), (IMP)studyImp, "v@:");
         if ([self.xiaowen respondsToSelector:@selector(study)]) {
             [self.xiaowen performSelector:@selector(study)];
         } else{
             NSLog(@"Sorry,I don't know");
         }
     }
    
     void studyImp(id self,SEL _cmd) {
         NSLog(@"i am from beijing");
     }
    

    (IMP)studyImp 意思是studyImp的地址指针;
    "v@:" 意思是,v代表无返回值void,如果是i则代表int;@代表 id self; : 代表 SEL _cmd;
    “v@:@@” 意思是,两个参数的没有返回值。

    1. KVC 字典转模型
    • 先实现最外层的属性转换
       // 创建对应模型对象
        id objc = [[self alloc] init];
    
        unsigned int count = 0;
    
        // 1.获取成员属性数组
        Ivar *ivarList = class_copyIvarList(self, &count);
    
        // 2.遍历所有的成员属性名,一个一个去字典中取出对应的value给模型属性赋值
        for (int i = 0; i < count; i++) {
    
            // 2.1 获取成员属性
            Ivar ivar = ivarList[i];
    
            // 2.2 获取成员属性名 C -> OC 字符串
            NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
    
            // 2.3 _成员属性名 => 字典key
            NSString *key = [ivarName substringFromIndex:1];
    
            // 2.4 去字典中取出对应value给模型属性赋值
            id value = dict[key];
    
            // 获取成员属性类型
            NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        }
    
    • 内层数组,字典的转换
          if ([value isKindOfClass:[NSDictionary class]] && ![ivarType containsString:@"NS"]) { 
                 //  是字典对象,并且属性名对应类型是自定义类型
                // 处理类型字符串 @\"User\" -> User
                ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
                ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
                // 自定义对象,并且值是字典
                // value:user字典 -> User模型
                // 获取模型(user)类对象
                Class modalClass = NSClassFromString(ivarType);
    
                // 字典转模型
                if (modalClass) {
                    // 字典转模型 user
                    value = [modalClass objectWithDict:value];
                }
    
            }
            
            if ([value isKindOfClass:[NSArray class]]) {
                // 判断对应类有没有实现字典数组转模型数组的协议
                if ([self respondsToSelector:@selector(arrayContainModelClass)]) {
    
                    // 转换成id类型,就能调用任何对象的方法
                    id idSelf = self;
    
                    // 获取数组中字典对应的模型
                    NSString *type =  [idSelf arrayContainModelClass][key];
    
                    // 生成模型
                    Class classModel = NSClassFromString(type);
                    NSMutableArray *arrM = [NSMutableArray array];
                    // 遍历字典数组,生成模型数组
                    for (NSDictionary *dict in value) {
                        // 字典转模型
                        id model =  [classModel objectWithDict:dict];
                        [arrM addObject:model];
                    }
    
                    // 把模型数组赋值给value
                    value = arrM;
    
                }
            }
    
    1. 动态的添加对象的成员变量和方法
    2. 实现NSCoding的自动归档和解档,我们把encodeWithCoder 和 initWithCoder这两个方法抽成宏
    #import "Person.h"
    #import <objc/runtime.h>
    
    #define encodeRuntime(A) \
    \
    unsigned int count = 0;\
    Ivar *ivars = class_copyIvarList([A class], &count);\
    for (int i = 0; i<count; i++) {\
    Ivar ivar = ivars[i];\
    const char *name = ivar_getName(ivar);\
    NSString *key = [NSString stringWithUTF8String:name];\
    id value = [self valueForKey:key];\
    [encoder encodeObject:value forKey:key];\
    }\
    free(ivars);\
    \
    
    #define initCoderRuntime(A) \
    \
    if (self = [super init]) {\
    unsigned int count = 0;\
    Ivar *ivars = class_copyIvarList([A class], &count);\
    for (int i = 0; i<count; i++) {\
    Ivar ivar = ivars[i];\
    const char *name = ivar_getName(ivar);\
    NSString *key = [NSString stringWithUTF8String:name];\
    id value = [decoder decodeObjectForKey:key];\
    [self setValue:value forKey:key];\
    }\
    free(ivars);\
    }\
    return self;\
    \
    
    @implementation Person
    
    - (void)encodeWithCoder:(NSCoder *)encoder {
        encodeRuntime(Person)
    }
    
    - (id)initWithCoder:(NSCoder *)decoder {
        initCoderRuntime(Person)
    }
    
    @end
    

    4、Runtime方法调用流程?

    1、当调用对象方法的时候,会通过obj_object的isa指针找对对应的归属类。
    2、从归属类(obj_class)类中的cache中寻找对应的相等的sel方法编号。
    3、如果没有找到,继续obj_class中的method_list中查找,如果找到写入cache中。
    4、如果没有到找到,会一直找到它的元类上。
    5、如果元类也没有的话,会调用消息动态解析方法+resovleInstanceMethod:+resloveClassMethod:的方法,查看是否存在绑定的方法。
    6、如果没有绑定方法,会调用消息转发方法-forwardingTargetForSelector:的方法。查看是否存在转发对象。
    7、如果没有存在消息转发对象,会调用-methodSignatureForSelector:的方法,查看是否有方法签名返回类型和参数类型。
    8、不存在签名方法和类型,就会来到-doseNotRecognizeSelector:方法内部程序crash提示无法识别选择器unrecognized selector sent to instance。
    9、存在签名的方法,就是继续执行-forwardInvocation:寻找IMP,没有找到IMP,就会来到-doseNotRecognizeSelector:方法内部程序crash提示无法识别选择器unrecognized selector sent to instance。

    @implementation Person
    - (BOOL)respondsToSelector:(SEL)aSelector {
        bool a= [super respondsToSelector:aSelector];
        return a;
    }
    //如果方法没有实现,默认返回false
    //如果返回false,就会走消息转发
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        bool a = [super resolveInstanceMethod:sel];
        return a;
    }
    //默认返回空
    //又被称为快速消息转发。
    // 如果为空,走慢速消息转发,继续转发消息
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        id a = [super forwardingTargetForSelector:aSelector];
        return a;
    }
    // 默认一般普通方法是返回空的。
    // 如果是协议方法,没有实现,不会反回空。
    //反回空,到这里就会崩溃了
    //如果这里返回了签名,会再次调用resolveInstanceMethod:(SEL)sel判断是否实现
    //如果仍然没有实现,就会走到fowardInvocation:
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        NSMethodSignature *a = [super methodSignatureForSelector:aSelector];
        return a;
    }
    //默认实现是崩溃
    //并且不能用try-catch捕获
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        [super forwardInvocation:anInvocation];
        NSLog(@"");
    }
    @end
    

    5、Runtime的方法交换的流程?

    1、方法交换要放在+(viod)load中处理。
    2、在load中首先使用class_addMethod的方法添加新方法。
    3、添加成功后,使用class_replaceMethod替换原来的方法。
    4、如果添加失败的话,则说明已经有添加成功。直接使用class_exchangeMethod的方法替换。
    5、在交换方法时候,使用dispach_one的方法。
    6、在新方法中调用新方法。

     使用注意要点:
     1、使用load时候,切记不要做初始化和大开销大内存逻辑。因为程序顺序是,父类->当前类->分类->mian
     2、使用的时候如果方法相同是不会覆盖原来的方法,会放在置顶,所以一般不会调用到原来的方法。
     3、在新方法中调用新方法。
    

    6、常见方法?

    • 获取属性列表
       objc_property_t *propertyList = class_copyPropertyList([self class], &count);
       for (unsigned int i=0; i<count; i++) {
           const char *propertyName = property_getName(propertyList[i]);
           NSLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]);
       }
    
    • 获取方法列表
       Method *methodList = class_copyMethodList([self class], &count);
       for (unsigned int i; i<count; i++) {
           Method method = methodList[i];
           NSLog(@"Method---->%@", NSStringFromSelector(method_getName(method)));
       }
    
    • 获取成员变量列表
      Ivar *ivarList = class_copyIvarList([self class], &count);
      for (unsigned int i; i<count; i++) {
          Ivar myIvar = ivarList[i];
          const char *ivarName = ivar_getName(myIvar);
          NSLog(@"Ivar---->%@", [NSString stringWithUTF8String:ivarName]);
      } 
    
    • 获取协议列表
       __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
       for (unsigned int i; i<count; i++) {
           Protocol *myProtocal = protocolList[i];
           const char *protocolName = protocol_getName(myProtocal);
           NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]);
       }
    

    计算机网络

    1、网络七层协议

    • 应用层:
      1.用户接口、应用程序;
      2.Application典型设备:网关;
      3.典型协议、标准和应用:TELNET、FTP、HTTP
    • 表示层:
      1.数据表示、压缩和加密presentation
      2.典型设备:网关
      3.典型协议、标准和应用:ASCLL、PICT、TIFF、JPEG|MPEG
      4.表示层相当于一个东西的表示,表示的一些协议,比如图片、声音和视频MPEG。
    • 会话层:
      1.会话的建立和结束;
      2.典型设备:网关;
      3.典型协议、标准和应用:RPC、SQL、NFS、X WINDOWS、ASP
    • 传输层:
      1.主要功能:端到端控制Transport;
      2.典型设备:网关;
      3.典型协议、标准和应用:TCP、UDP、SPX
    • 网络层:
      1.主要功能:路由、寻址Network;
      2.典型设备:路由器;
      3.典型协议、标准和应用:IP、IPX、APPLETALK、ICMP;
    • 数据链路层:
      1.主要功能:保证无差错的疏忽链路的data link;
      2.典型设备:交换机、网桥、网卡;
      3.典型协议、标准和应用:802.2、802.3ATM、HDLC、FRAME RELAY;
    • 物理层:
      1.主要功能:传输比特流Physical;
      2.典型设备:集线器、中继器
      3.典型协议、标准和应用:V.35、EIA/TIA-232.

    2、Http 和 Https 的区别?Https为什么更加安全?

    • 区别
      1.HTTPS 需要向机构申请 CA 证书,极少免费。
      2.HTTP 属于明文传输,HTTPS基于 SSL 进行加密传输。
      3.HTTP 端口号为 80,HTTPS 端口号为 443 。
      4.HTTPS 是加密传输,有身份验证的环节,更加安全。
    • 安全
      SSL(安全套接层) TLS(传输层安全)
      以上两者在传输层之上,对网络连接进行加密处理,保障数据的完整性,更加的安全。

    3、HTTPS的连接建立流程?

    • 服务器端的公钥和私钥,用来进行非对称加密
    • 客户端生成的随机密钥,用来进行对称加密
    http建立连接过程.jpg

    如上图,HTTPS连接过程大致可分为八步:

    1. 客户端访问HTTPS连接。
      客户端会把安全协议版本号、客户端支持的加密算法列表、随机数C发给服务端。

    2. 服务端发送证书给客户端

    • 服务端接收密钥算法配件后,会和自己支持的加密算法列表进行比对,如果不符合,则断开连接。否则,服务端会在该算法列表中,选择一种对称算法(如AES)、一种公钥算法(如具有特定秘钥长度的RSA)和一种MAC算法发给客户端。

    • 服务器端有一个密钥对,即公钥和私钥,是用来进行非对称加密使用的,服务器端保存着私钥,不能将其泄露,公钥可以发送给任何人。

    • 在发送加密算法的同时还会把数字证书和随机数S发送给客户端

    1. 客户端验证server证书
      会对server公钥进行检查,验证其合法性,如果发现发现公钥有问题,那么HTTPS传输就无法继续。

    2. 客户端组装会话秘钥
      如果公钥合格,那么客户端会用服务器公钥来生成一个前主秘钥(Pre-Master Secret,PMS),并通过该前主秘钥和随机数C、S来组装成会话秘钥

    3. 客户端将前主秘钥加密发送给服务端
      是通过服务端的公钥来对前主秘钥进行非对称加密,发送给服务端

    4. 服务端通过私钥解密得到前主秘钥
      服务端接收到加密信息后,用私钥解密得到主秘钥。

    5. 服务端组装会话秘钥
      服务端通过前主秘钥和随机数C、S来组装会话秘钥。
      至此,服务端和客户端都已经知道了用于此次会话的主秘钥。

    6. 数据传输
      客户端收到服务器发送来的密文,用客户端密钥对其进行对称解密,得到服务器发送的数据。
      同理,服务端收到客户端发送来的密文,用服务端密钥对其进行对称解密,得到客户端发送的数据。

    4、解释一下三次握手和四次挥手?

    • 三次握手
      1.由客户端向服务端发送 SYN 同步报文。
      2.当服务端收到 SYN 同步报文之后,会返回给客户端 SYN 同步报文和 ACK 确认报文。
      3.客户端会向服务端发送 ACK 确认报文,此时客户端和服务端的连接正式建立。

    • 建立连接
      1.这个时候客户端就可以通过 Http 请求报文,向服务端发送请求
      2.服务端接收到客户端的请求之后,向客户端回复 Http 响应报文。

    • 四次挥手
      1.先由客户端向服务端发送 FIN 结束报文。
      2.服务端会返回给客户端 ACK 确认报文 。此时,由客户端发起的断开连接已经完成。
      3.服务端会发送给客户端 FIN 结束报文 和 ACK 确认报文。
      4.客户端会返回 ACK 确认报文到服务端,至此,由服务端方向的断开连接已经完成。

    5、TCP 和 UDP的区别?

    • TCP:面向连接、传输可靠(保证数据正确性,保证数据顺序)、用于传输大量数据(流模式)、速度慢,建立连接需要开销较多(时间,系统资源)。
    • UDP:面向非连接、传输不可靠、用于传输少量数据(数据包模式)、速度快。

    多线程

    1、进程与线程

    • 进程:
      1.进程是一个具有一定独立功能的程序关于某次数据集合的一次运行活动,它是操作系统分配资源的基本单元.
      2.进程是指在系统中正在运行的一个应用程序,就是一段程序的执行过程,我们可以理解为手机上的一个app.
      3.每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内,拥有独立运行所需的全部资源

    • 线程:
      1.程序执行流的最小单元,线程是进程中的一个实体.
      2.一个进程要想执行任务,必须至少有一条线程.应用程序启动的时候,系统会默认开启一条线程,也就是主线程

    • 进程和线程的关系
      1.线程是进程的执行单元,进程的所有任务都在线程中执行
      2.线程是 CPU 分配资源和调度的最小单位
      3.一个程序可以对应多个进程(多进程),一个进程中可有多个线程,但至少要有一条线程
      4.同一个进程内的线程共享进程资源

    2、什么是多线程?

    • 多线程的实现原理:事实上,同一时间内单核的CPU只能执行一个线程,多线程是CPU快速的在多个线程之间进行切换(调度),造成了多个线程同时执行的假象。
    • 如果是多核CPU就真的可以同时处理多个线程了。
    • 多线程的目的是为了同步完成多项任务,通过提高系统的资源利用率来提高系统的效率。

    3、多线程的优点和缺点?

    • 优点:
      能适当提高程序的执行效率
      能适当提高资源利用率(CPU、内存利用率)

    • 缺点:

    1. 开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能
    2. 线程越多,CPU在调度线程上的开销就越大
    3. 程序设计更加复杂:比如线程之间的通信、多线程的数据共享

    4、多线程的并行并发有什么区别?

    • 并行:充分利用计算机的多核,在多个线程上同步进行
    • 并发:在一条线程上通过快速切换,让人感觉在同步进行

    5、iOS中实现多线程的几种方案,各自有什么特点?

    • NSThread 面向对象的,需要程序员手动创建线程,但不需要手动销毁。子线程间通信很难。
    • GCD C语言,充分利用了设备的多核,自动管理线程生命周期。比NSOperation效率更高。
    • NSOperation 基于GCD封装,更加面向对象,比GCD多了一些功能。

    6、多个网络请求完成后执行下一步?

    使用GCD的dispatch_group_t

    创建一个dispatch_group_t

    每次网络请求前先dispatch_group_enter,请求回调后再dispatch_group_leave,enter和leave必须配合使用,有几次enter就要有几次leave,否则group会一直存在。

    当所有enter的block都leave后,会执行dispatch_group_notify的block。

    栅栏函数中传入的参数队列必须是由 dispatch_queue_create 方法创建的队列,否则,与 dispatch_async 无异,起不到“栅栏”的作用。

    NSString *str = @"http://xxxx.com/";
    NSURL *url = [NSURL URLWithString:str];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    NSURLSession *session = [NSURLSession sharedSession];
    
    dispatch_group_t downloadGroup = dispatch_group_create();
    for (int i=0; i<10; i++) {
        dispatch_group_enter(downloadGroup);
    
        NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            NSLog(@"%d---%d",i,i);
            dispatch_group_leave(downloadGroup);
        }];
        [task resume];
    }
    
    dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{
        NSLog(@"end");
    });
    

    使用GCD的信号量dispatch_semaphore_t

    dispatch_semaphore信号量为基于计数器的一种多线程同步机制。如果semaphore计数大于等于1,计数-1,返回,程序继续运行。如果计数为0,则等待。dispatch_semaphore_signal(semaphore)为计数+1操作,dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER)为设置等待时间,这里设置的等待时间是一直等待。

    创建semaphore为0,等待,等10个网络请求都完成了,dispatch_semaphore_signal(semaphore)为计数+1,然后计数-1返回

    NSString *str = @"http://xxxx.com/";
    NSURL *url = [NSURL URLWithString:str];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    NSURLSession *session = [NSURLSession sharedSession];
    
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    for (int i=0; i<10; i++) {
    
        NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            NSLog(@"%d---%d",i,i);
            count++;
            if (count==10) {
                dispatch_semaphore_signal(sem);
                count = 0;
            }
        }];
        [task resume];
    }
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"end");
    });
    

    7、多个网络请求顺序执行后执行下一步?

    使用信号量semaphore

    每一次遍历,都让其dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER),这个时候线程会等待,阻塞当前线程,直到dispatch_semaphore_signal(sem)调用之后

    NSString *str = @"http://www.jianshu.com/p/6930f335adba";
    NSURL *url = [NSURL URLWithString:str];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    NSURLSession *session = [NSURLSession sharedSession];
    
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    for (int i=0; i<10; i++) {
    
        NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    
            NSLog(@"%d---%d",i,i);
            dispatch_semaphore_signal(sem);
        }];
    
        [task resume];
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    }
    
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"end");
    });
    

    8、异步操作两组数据时, 执行完第一组之后, 才能执行第二组?

    这里使用dispatch_barrier_async栅栏方法即可实现

    dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{
        NSLog(@"第一次任务的主线程为: %@", [NSThread currentThread]);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"第二次任务的主线程为: %@", [NSThread currentThread]);
    });
    
    dispatch_barrier_async(queue, ^{
        NSLog(@"第一次任务, 第二次任务执行完毕, 继续执行");
    });
    
    dispatch_async(queue, ^{
        NSLog(@"第三次任务的主线程为: %@", [NSThread currentThread]);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"第四次任务的主线程为: %@", [NSThread currentThread]);
    });
    

    9、多线程中的死锁?

    死锁是由于多个线程(进程)在执行过程中,因为争夺资源而造成的互相等待现象,你可以理解为卡主了。产生死锁的必要条件有四个:

    • 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
    • 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
    • 不可剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
    • 环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

    最常见的就是 同步函数 + 主队列 的组合,本质是队列阻塞。

    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"2");
    });
    
    NSLog(@"1");
    // 什么也不会打印,直接报错
    

    10、GCD执行原理?

    • GCD有一个底层线程池,这个池中存放的是一个个的线程。之所以称为“池”,很容易理解出这个“池”中的线程是可以重用的,当一段时间后这个线程没有被调用的话,这个线程就会被销毁。注意:开多少条线程是由底层线程池决定的(线程建议控制再3~5条),池是系统自动来维护,不需要我们程序员来维护,我们只关心的是向队列中添加任务,队列调度即可。
    • 如果队列中存放的是同步任务,则任务出队后,底层线程池中会提供一条线程供这个任务执行,任务执行完毕后这条线程再回到线程池。这样队列中的任务反复调度,因为是同步的,所以当我们用currentThread打印的时候,就是同一条线程。
    • 如果队列中存放的是异步的任务,(注意异步可以开线程),当任务出队后,底层线程池会提供一个线程供任务执行,因为是异步执行,队列中的任务不需等待当前任务执行完毕就可以调度下一个任务,这时底层线程池中会再次提供一个线程供第二个任务执行,执行完毕后再回到底层线程池中。
    • 这样就对线程完成一个复用,而不需要每一个任务执行都开启新的线程,也就从而节约的系统的开销,提高了效率。在iOS7.0的时候,使用GCD系统通常只能开5-8条线程,iOS8.0以后,系统可以开启很多条线程,但是实在开发应用中,建议开启线程条数:3-5条最为合理。

    项目架构

    1、MVC、MVVM模式

    MVC(Model、View、Controller)

    MVC是比较直观的架构模式,最核心的就是通过Controller层来进行调控
    Model和View永远不能相互通信,只能通过Controller传递
    Controller可以直接与Model对话(读写调用Model),Model通过Notification和KVO机制与Controller间接通信
    Controller可以直接与View对话,通过IBoutlet直接操作View,IBoutlet直接对应View的控件(例如创建一个Button:需声明一个 IBOutlet UIButton * btn),View通过action向Controller报告时间的发生(用户点击了按钮)。Controller是View的直接数据源

    • 优点: 对于混乱的项目组织方式,有了一个明确的组织方式。通过Controller来掌控全局,同时将View展示和Model的变化分开
    • 缺点: 愈发笨重的Controller,随着业务逻辑的增加,大量的代码放进Controller,导致Controller越来越臃肿,堆积成千上万行代码,后期维护起来费时费力
    MVVM(Model、Controller/View、ViewModel)

    在MVVM中,View和ViewController联系在一起,我们把它们视为一个组件,View和ViewController都不能直接引用model,而是引用是视图模型即ViewModel。 ViewModel是一个用来放置用户输入验证逻辑、视图显示逻辑、网络请求等业务逻辑的地方,这样的设计模式,会轻微增加代码量,但是会减少代码的复杂性

    • 优点: View可以独立于Model的变化和修改,一个ViewModel可以绑定到不同的View上,降低耦合,增加重用
    • 缺点: 过于简单的项目不适用、大型的项目视图状态较多时构建和维护成本太大
      合理的运用架构模式有利于项目、团队开发工作,不同的设计模式,只是让不同的场景有了更多的选择方案。根据项目场景和开发需求,选择最合适的解决方案。

    调试技巧

    1、LLDB常用的调试命令?

    po:print object的缩写,表示显示对象的文本描述,如果对象不存在则打印nil。
    p:可以用来打印基本数据类型。
    call:执行一段代码 如:call NSLog(@"%@", @"yang")
    expr:动态执行指定表达式
    bt:打印当前线程堆栈信息 (bt all 打印所有线程堆栈信息)
    image:常用来寻找栈地址对应代码位置 如:image lookup --address 0xxxx

    2、断点调试?

    条件断点
    打上断点之后,对断点进行编辑,设置相应过滤条件。下面简单的介绍一下条件设置:

    1. Condition:返回一个布尔值,当布尔值为真触发断点,一般里面我们可以写一个表达式。
    2. Ignore:忽略前N次断点,到N+1次再触发断点。
    3. Action:断点触发事件,分为六种:
    • AppleScript:执行脚本。
    • Capture GPU Frame:用于OpenGL ES调试,捕获断点处GPU当前绘制帧。
    • Debugger Command:和控制台中输入LLDB调试命令一致。
    • Log Message:输出自定义格式信息至控制台。
    • Shell Command:接收命令文件及相应参数列表,Shell Command是异步执行的,只有勾选“Wait until done”才会等待Shell命令执行完在执行调试。
    • Sound:断点触发时播放声音。
    • Options(Automatically continue after evaluating actions选项):选中后,表示断点不会终止程序的运行。

    异常断点
    异常断点可以快速定位不满足特定条件的异常,比如常见的数组越界,这时候很难通过异常信息定位到错误所在位置。这个时候异常断点就可以发挥作用了。
    Exception:可以选择抛出异常对象类型:OC或C++。
    Break:选择断点接收的抛出异常来源是Throw还是Catch语句。

    符号断点
    符号断点的创建方式和异常断点一样一样的,在符号断点中可以指定要中断执行的方法:
    Symbol:[类名 方法名]可以执行到指定类的指定方法中开始断点。

    3、iOS 常见的崩溃类型有哪些?

    • unrecognized selector crash
    • KVO crash
    • NSNotification crash
    • NSTimer crash
    • Container crash
    • NSString crash
    • Bad Access crash (野指针)
    • UI not on Main Thread Crash

    性能优化

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

    1. 最常用的就是cell的重用, 注册重用标识符
    • 如果不重用cell时,每当一个cell显示到屏幕上时,就会重新创建一个新的cell
    • 如果重用cell,为cell创建一个ID,每当需要显示cell 的时候,都会先去缓冲池中寻找可循环利用的cell,如果没有再重新创建cell
    1. 避免cell的重新布局
    • cell的布局填充等操作比较耗时,一般创建时就布局好
    1. 提前计算并缓存cell的属性及内容
    • 当我们创建cell的数据源方法时,编译器并不是先创建cell 再定cell的高度
    • 而是先根据内容一次确定每一个cell的高度,高度确定后,再创建要显示的cell,滚动时,每当cell进入凭虚都会计算高度,提前估算高度告诉编译器,编译器知道高度后,紧接着就会创建cell,这时再调用高度的具体计算方法,这样可以方式浪费时间去计算显示以外的cell
    1. 减少cell中控件的数量
    • 尽量使cell得布局大致相同,不同风格的cell可以使用不用的重用标识符,初始化时添加控件,
    • 不适用的可以先隐藏
    1. 不要使用ClearColor,无背景色,透明度也不要设置为0
    • 渲染耗时比较长
    1. 使用局部更新
    • 如果只是更新某组的话,使用reloadSection进行局部更
    1. 加载网络数据,下载图片,使用异步加载,并缓存
    2. 少使用addView 给cell动态添加view
    3. 按需加载cell,cell滚动很快时,只加载范围内的cell
    4. 不要实现无用的代理方法,tableView只遵守两个协议
    5. 缓存行高:estimatedHeightForRow不能和HeightForRow里面的layoutIfNeed同时存在,这两者同时存在才会出现“窜动”的bug。所以我的建议是:只要是固定行高就写预估行高来减少行高调用次数提升性能。如果是动态行高就不要写预估方法了,用一个行高的缓存字典来减少代码的调用次数即可
    6. 不要做多余的绘制工作。在实现drawRect:的时候,它的rect参数就是需要绘制的区域,这个区域之外的不需要进行绘制。例如上例中,就可以用CGRectIntersectsRect、CGRectIntersection或CGRectContainsRect判断是否需要绘制image和text,然后再调用绘制方法。
    7. 预渲染图像。当新的图像出现时,仍然会有短暂的停顿现象。解决的办法就是在bitmap context里先将其画一遍,导出成UIImage对象,然后再绘制到屏幕;
    8. 使用正确的数据结构来存储数据。

    2、如何提升 tableview 的流畅度?

    本质上是降低 CPU、GPU 的工作,从这两个大的方面去提升性能。
    CPU:对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制
    GPU:纹理的渲染

    1. 卡顿优化在 CPU 层面
    • 尽量用轻量级的对象,比如用不到事件处理的地方,可以考虑使用 CALayer 取代 UIView
    • 不要频繁地调用 UIView 的相关属性,比如 frame、bounds、transform 等属性,尽量减少不必要的修改
    • 尽量提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性
    • Autolayout 会比直接设置 frame 消耗更多的 CPU 资源
    • 图片的 size 最好刚好跟 UIImageView 的 size 保持一致
    • 控制一下线程的最大并发数量
    • 尽量把耗时的操作放到子线程
    • 文本处理(尺寸计算、绘制)
    • 图片处理(解码、绘制)
    1. 卡顿优化在 GPU层面
    • 尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示
    • GPU能处理的最大纹理尺寸是 4096x4096,一旦超过这个尺寸,就会占用 CPU 资源进行处理,所以纹理尽量不要超过这个尺寸
    • 尽量减少视图数量和层次
    • 减少透明的视图(alpha<1),不透明的就设置 opaque 为 YES
    • 尽量避免出现离屏渲染

    3、iOS 保持界面流畅的技巧?

    1. 预排版,提前计算
    • 在接收到服务端返回的数据后,尽量将 CoreText 排版的结果、单个控件的高度、cell 整体的高度提前计算好,将其存储在模型的属性中。需要使用时,直接从模型中往外取,避免了计算的过程。
    • 尽量少用 UILabel,可以使用 CALayer 。避免使用 AutoLayout 的自动布局技术,采取纯代码的方式
    1. 预渲染,提前绘制
    • 例如圆形的图标可以提前在,在接收到网络返回数据时,在后台线程进行处理,直接存储在模型数据里,回到主线程后直接调用就可以了
    • 避免使用 CALayer 的 Border、corner、shadow、mask 等技术,这些都会触发离屏渲染。
    1. 异步绘制
    2. 全局并发线程
    3. 高效的图片异步加载

    4、APP启动时间应从哪些方面优化?

    App启动时间可以通过xcode提供的工具来度量,在Xcode的Product->Scheme-->Edit Scheme->Run->Auguments中,将环境变量DYLD_PRINT_STATISTICS设为YES,优化需以下方面入手

    1. dylib loading time
    • 核心思想是减少dylibs的引用
    • 合并现有的dylibs(最好是6个以内)
    • 使用静态库
    1. rebase/binding time
    • 核心思想是减少DATA块内的指针
    • 减少Object C元数据量,减少Objc类数量,减少实例变量和函数(与面向对象设计思想冲突)
    • 减少c++虚函数
    • 多使用Swift结构体(推荐使用swift)
    1. ObjC setup time
    • 核心思想同上,这部分内容基本上在上一阶段优化过后就不会太过耗时
    • initializer time
    1. 使用initialize替代load方法
    • 减少使用c/c++的attribute((constructor));推荐使用dispatch_once() pthread_once() std:once()等方法
    • 推荐使用swift
    • 不要在初始化中调用dlopen()方法,因为加载过程是单线程,无锁,如果调用dlopen则会变成多线程,会开启锁的消耗,同时有可能死锁
    • 不要在初始化中创建线程

    相关文章

      网友评论

        本文标题:iOS2023年最新面试题(持续更新中)

        本文链接:https://www.haomeiwen.com/subject/zzxjkdtx.html