iOS 中高级面试题(附答案)

RunLoop

1、什么是 RunLoop? RunLoop 作用有哪些?

  • RunLoop 可以称之为运行循环,在程序运行过程中循环做一些事情,如果没有 RunLoop 程序执行完毕就会立即退出,有 RunLoop 程序会一直运行,并且时时刻刻在等待用户的输入操作。RunLoop可以在需要的时候自己跑起来运行,在没有操作的时候就停下来休息。充分节省CPU资源,提高程序性能。
  • 基本作用:
    1. 保持程序持续运行。程序一启动就会开一个主线程,主线程一开起来就会跑一个主线程对应的 RunLoopRunLoop 保证主线程不会被销毁,也就保证了程序的持续运行。
    2. 处理App中的各种事件(比如:触摸事件,定时器事件,Selector 事件等) 。
    3. 节省CPU资源,提高程序性能。程序运行起来时,当什么操作都没有做的时候,RunLoop 就告诉 CPU,现在没有事情做,我要去休息,这时 CPU 就会将其资源释放出来去做其他的事情,当有事情做的时候 RunLoop 就会立马起来去做事情。

2、app 如何接收到触摸事件的 ?

  1. APP进程的mach port接收来自SpringBoard的触摸事件,主线程的RunLoop被唤醒,触发source1回调。
  2. source1回调又触发了一个source0回调,将接收到的IOHIDEvent对象封装成UIEvent对象,此时APP将正式开始对于触摸事件的响应
  3. source0回调将触摸事件添加到UIApplication的事件队列,当触摸事件出队UIApplication为触摸事件寻找最佳响应者。
  4. 寻找到最佳响应者之后,接下来的事情便是事件在响应链中传递和响应
    那么事件响应链是如何传递的呢 ? 可简称为 “由父及子” 的过程,即:
    • 触摸事件的传递是从父控件传递到子控件
    • 也就是从UIApplicaiton->window->寻找处理事件的最合适的view
      两个重要的方法:
      // 获取响应 事件的视图,通过下面的方法判断触控点位置 
      - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
      
      // 判断触摸点是不是在这个view的坐标上。如果在坐标上,会分发事件给这个view的子view。后每个子view重复以上步骤,直至最底层的一个合适的view。
      - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
      

在这里插入图片描述

那么事件响应链是如何响应的呢?可简称为 “由子及父” 的过程,即:

  • 事件响应会先从底层最合适的view开始,然后随着上一步找到的链一层一层响应touch事件。默认touch事件会传递给上一层。
  • 如果到了viewControllerview,就会传递给viewController
  • 如果viewController不能处理,就会传递给UIWindow
  • 如果UIWindow无法处理,就会传递给UIApplication
  • 如果UIApplication无法处理,就会传递给UIApplicationDelegate
  • 如果UIApplicationDelegate不能处理,则会丢弃该事件。
    在这里插入图片描述

3、为什么只有主线程的RunLoop是开启的?

app启动前会调用main函数,具体如下:

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

mian函数中调用UIApplicationMain,这里会创建一个主线程用于UI处理,为了让程序可以一直运行,所以在主线程中开启一个RunLoop,让主线程常驻

4、为什么只在主线程刷新 UI ?

  1. UIKit 并不是一个线程安全的类,UI操作涉及到渲染访问各种View对象的属性,如果异步操作下会存在读写问题,而为其加锁则会耗费大量资源并拖慢运行速度

  2. 另一方面因为整个程序的起点 UIApplication 是在主线程进行初始化所有的用户事件都是在主线程上进行传递(如点击、拖动),所以view只能在主线程上才能对事件进行响应而在渲染方面由于图像的渲染需要以60帧的刷新率在屏幕上同时更新,在非主线程异步化的情况下无法确定这个处理过程能够实现同步更新。

5、PerformSelectorRunLoop的关系 ?

  1. 当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

  2. 当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

6、如何使线程保活?

  1. NSThread执行的方法中添加while(true){},这样是模拟 RunLoop 的运行原理,结合GCD 的信号量,在 {} 中处理任务。

  2. 采用 RunLoop 的方式。参考这篇文章
    让子线程永远活着,这时就要用到常驻线程:给子线程开启一个 RunLoop
    注意:子线程执行完操作之后就会立即释放,即使我们使用强引用引用子线程使子线程不被释放,也不能给子线程再次添加操作,或者再次开启。
    子线程开启 RunLoop 的代码,先点击屏幕开启子线程并开启子线程 RunLoop

    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
       // 创建子线程并开启
        NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(show) object:nil];
        self.thread = thread;
        [thread start];
    }
    -(void)show {
        // 注意:打印方法一定要在RunLoop创建开始运行之前,如果在RunLoop跑起来之后打印,RunLoop先运行起来,已经在跑圈了就出不来了,进入死循环也就无法执行后面的操作了。
        // 但是此时点击Button还是有操作的,因为Button是在RunLoop跑起来之后加入到子线程的,当Button加入到子线程RunLoop就会跑起来
        NSLog(@"%s",__func__);
        // 1.创建子线程相关的RunLoop,在子线程中创建即可,并且RunLoop中要至少有一个Timer 或 一个Source 保证RunLoop不会因为空转而退出,因此在创建的时候直接加入
        // 添加Source [NSMachPort port] 添加一个端口
        [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        // 添加一个Timer
        NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];    
        // 创建监听者
        CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
            switch (activity) {
                case kCFRunLoopEntry:
                    NSLog(@"RunLoop进入");
                    break;
                case kCFRunLoopBeforeTimers:
                    NSLog(@"RunLoop要处理Timers了");
                    break;
                case kCFRunLoopBeforeSources:
                    NSLog(@"RunLoop要处理Sources了");
                    break;
                case kCFRunLoopBeforeWaiting:
                    NSLog(@"RunLoop要休息了");
                    break;
                case kCFRunLoopAfterWaiting:
                    NSLog(@"RunLoop醒来了");
                    break;
                case kCFRunLoopExit:
                    NSLog(@"RunLoop退出了");
                    break;
                
                default:
                    break;
            }
        });
        // 给RunLoop添加监听者
        CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
        // 2.子线程需要开启RunLoop
        [[NSRunLoop currentRunLoop]run];
        CFRelease(observer);
    }
    - (IBAction)btnClick:(id)sender {
    	// 用常驻线程处理事情
        [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
    }
    -(void)test
    {
        NSLog(@"%@",[NSThread currentThread]);
    }
    

    注意: 创建子线程相关的 RunLoop ,在子线程中创建即可,并且 RunLoop 中要至少有一个 Timer 或 一个 Source 保证 RunLoop 不会因为空转而退出,因此在创建的时候直接加入。如果没有加入 Timer 或者 Source ,或者只加入一个监听者,运行程序会崩溃。

7、子线程默认有RunLoop吗? RunLoop 创建和销毁的时机又是什么时候呢?

  • 线程和 RunLoop 之间是一一对应的。但是在创建子线程时,子线程的 RunLoop 需要我们主动创建 。只需在子线程中获取当前线程的 RunLoop 对象即可 [NSRunLoop currentRunLoop] ;如果不获取,那子线程就不会创建与之相关联的 RunLoop
  • RunLoop 在第一次获取时创建,在线程结束时销毁。

8、RunLoop有哪些 Mode 呢?滑动时发现定时器没有回调,是因为什么原因呢?

  • 系统默认注册了5Mode
    1. kCFRunLoopDefaultMode :App的默认Mode,通常主线程是在这个Mode下运行
    2. UITrackingRunLoopMode :界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
    3. UIInitializationRunLoopMode : 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode
    4. GSEventReceiveRunLoopMode : 接受系统事件的内部 Mode,通常用不到
    5. kCFRunLoopCommonModes : 这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode 
    
  • 因为 App 为了响应 CFRunLoopSourceRef 事件源, RunLoop 会进行 Mode 切换以响应不同操作
    因为如果我们在主线程使用定时器,此时 RunLoopModekCFRunLoopDefaultMode ,即定时器属于 kCFRunLoopDefaultMode那么此时我们滑动 ScrollView 时, RunLoopMode 会切换到 UITrackingRunLoopMode ,因此在主线程的定时器就不在管用了,调用的方法也就不再执行了,当我们停止滑动时, RunLoopMode 切换回 kCFRunLoopDefaultMode ,所以 NSTimer 就又管用了。
    为了防止此类情况发生,我们会将定时器加入 RunLoop 中,并设置 RunLoopModeNSRunLoopCommonModes
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
    // 加入到RunLoop中才可以运行
    // 因此也就是说如果我们使用NSRunLoopCommonModes,timer可以在UITrackingRunLoopMode,kCFRunLoopDefaultMode两种模式下运行
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    

KVO

1、KVO 实现原理

  1. KVO是关于RunTime机制实现的

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

  3. 如果原类为Person,那么生成的派生类名为NSKVONotifying_Person

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

  5. 键值观察通知依赖于NSObject的两个方法:willChangeValueForKey:didChangeValueForKey:,在一个被观察属性发生改变之前,willChangeValueForKey:一定会被调用,这就会记录旧的值。而当改变发生后,didChangeValueForKey:会被调用,继而observeValueForKey:ofObject:change:context:也会被调用。且重写观察属性的setter方法这种继承方式的注入在运行时而不是编译时实现的。

2、如何手动关闭 KVO ?

  1. 重写被观察对象的automaticallyNotifiesObserversForKey方法,返回NO

  2. 重写automaticallyNotifiesObserversOf ,返回NO。

    注意:关闭 kvo 后,需要手动在赋值前后添加willChangeValueForKeydidChangeValueForKey,才可以收到观察通知。

3、通过 KVC 修改属性会触发 KVO 吗?

会触发。即使没有 setter 方法也会触发。

4、哪些情况下使用 kvo 会崩溃,怎么防护崩溃?

  • removeObserver一个未注册的keyPath,导致错误:Cannot remove an observer A for the key path “str”,because it is not registered as an observer。解决办法:根据实际情况,增加一个添加keyPath的标记,在dealloc中根据这个标记,删除观察者。

  • 添加的观察者已经销毁,但是并未移除这个观察者,当下次这个观察的keyPath发生变化时,kvo中的观察者的引用变成了野指针,导致crash。 解决办法:在观察者即将销毁的时候,先移除这个观察者。

其实还可以将观察者observer委托给另一个类去完成,这个类弱引用被观察者,当这个类销毁的时候,移除观察者对象。参考KVOController

5、KVO 的优缺点?

优点:

  1. 能够提供一种简单的方法实现两个对象间的同步。例如:model和view之间同步

  2. 能够对非我们创建的对象,即内部对象的状态改变作出响应,而且不需要改变内部对象(SKD对象)的实现

  3. 能够提供观察的属性的最新值以及先前值

  4. 用key paths来观察属性,因此也可以观察嵌套对象

  5. 完成了对观察对象的抽象,因为不需要额外的代码来允许观察值能够被观察

缺点:

  1. 我们观察的属性必须使用strings来定义。因此在编译器不会出现警告以及检查

  2. 对属性重构将导致我们的观察代码不再可用

  3. 复杂的if语句要求对象正在观察多个值。这是因为所有的观察代码通过一个方法来指向

  4. 当释放观察者时不需要移除观察者

RunTime

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

  • 对象:OC中的对象指向的是一个objc_object指针类型typedef struct objc_object *id;从它的结构体中可以看出,它包括一个isa指针,指向的是这个对象的类对象,一个对象实例就是通过这个isa找到它自己的Class,而这个Class中存储的就是这个实例的方法列表属性列表成员变量列表等相关信息的。

    /// Represents an instance of a class.
    struct objc_object {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    };
    
  • 类:在OC中的类是用Class来表示的,实际上它指向的是一个objc_class的指针类型,typedef struct objc_class *Class;对应的结构体如下:

struct objc_class {
      Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

  #if !__OBJC2__
      Class _Nullable super_class                              OBJC2_UNAVAILABLE;
      const char * _Nonnull name                               OBJC2_UNAVAILABLE;
      long version                                             OBJC2_UNAVAILABLE;
      long info                                                OBJC2_UNAVAILABLE;
      long instance_size                                       OBJC2_UNAVAILABLE;
      struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
      struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
      struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
      struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
  #endif

  }

从结构体中定义的变量可知,OC的Class类型包括如下数据(即:元数据metadata):super_class(父类类对象);name(类对象的名称);versioninfo(版本和相关信息);instance_size(实例内存大小);ivars(实例变量列表);methodLists(方法列表);cache(缓存);protocols(实现的协议列表);
当然也包括一个isa指针,这说明Class也是一个对象类型,所以我们称之为类对象,这里的isa指向的是元类对象(metaclass),元类中保存了创建类对象(Class)的类方法的全部信息
以下图中可以清楚的了解到OC对象、类、元类之间的关系
aHR0cHM6Ly9pbWcyMDIwLmNuYmxvZ3MuY29tL2Jsb2cvOTA3MjU5LzIwMjAwMy85MDcyNTktMjAyMDAzMDUxMTEwMjM1MDYtOTkxOTU1MTQzLnBuZw.png

从图中可知:对象的isa指针指向类对象的isa指针指向元类元类对象的isa指针指向根元类根元类的isa指针指向他本身,从而形成一个闭环。
元类(Meta Class):是一个类对象的类,即:Class的类,这里保存了类方法等相关信息。
我们再看一下类对象中存储的方法、属性、成员变量等信息的结构体:

  • objc_ivar_list :

    存储了类的成员变量,可以通过object_getIvarclass_copyIvarList获取;另外这两个方法是用来获取类的属性列表的class_getPropertyclass_copyPropertyList,属性和成员变量是有区别的。

    struct objc_ivar {
        char * _Nullable ivar_name                               OBJC2_UNAVAILABLE;
        char * _Nullable ivar_type                               OBJC2_UNAVAILABLE;
        int ivar_offset                                          OBJC2_UNAVAILABLE;
    #ifdef __LP64__
        int space                                                OBJC2_UNAVAILABLE;
    #endif
    }                                                            OBJC2_UNAVAILABLE;

    struct objc_ivar_list {
        int ivar_count                                           OBJC2_UNAVAILABLE;
    #ifdef __LP64__
        int space                                                OBJC2_UNAVAILABLE;
    #endif
        /* variable length structure */
        struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;
    }
  • objc_method_list :

    存储了类的方法列表,可以通过class_copyMethodList获取。结构体如下:

    struct objc_method {
        SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
        char * _Nullable method_types                            OBJC2_UNAVAILABLE;
        IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
    }                                                            OBJC2_UNAVAILABLE;

    struct objc_method_list {
        struct objc_method_list * _Nullable obsolete             OBJC2_UNAVAILABLE;
    		int method_count                                         OBJC2_UNAVAILABLE;
    #ifdef __LP64__
        int space                                                OBJC2_UNAVAILABLE;
    #endif
        /* variable length structure */
        struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
    }
  • objc_protocol_list :

    储存了类的协议列表,可以通过class_copyProtocolList获取。结构体如下:

  struct objc_protocol_list {
      struct objc_protocol_list * _Nullable next;
      long count;
      __unsafe_unretained Protocol * _Nullable list[1];
  };

2、为什么要设计 metaclass

metaclass 代表的是类对象的对象,存储了类的类方法,目的是将实例和类的相关方法列表以及构建信息区分开来,方便各司其职,符合单一职责设计原则

3、class_copyIvarList & class_copyPropertyList区别?

class_copyIvarList:获取的是类的成员变量列表,即:@interface{中声明的变量}

class_copyPropertyList:获取的是类的属性列表,即:通过@property声明的属性

4、class_rw_tclass_ro_t 的区别?

class_rw_t:代表的是可读写的内存区,这块区域中存储的数据是可以更改的。

class_ro_t:代表的是只读的内存区,这块区域中存储的数据是不可以更改的。

OC对象中存储的属性方法遵循的协议数据其实被存储在这两块儿内存区域而我们通过RunTime动态修改类的方法时,是修改在class_rw_t区域中存储的方法列表

5、category如何被加载的?两个 categoryload方法的加载顺序?两个 category 的同名方法的加载顺序?

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

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

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

6、category & extension区别?能给 NSObject 添加 Extension 吗?结果如何?

category :分类

  1. 给类添加新的方法
  2. 不能给类添加成员变量
  3. 通过@property定义的变量,只能生成对应的getter和setter的方法声明,但是不能实现getter和setter方法,同时也不能生成带下划线的成员属性
  4. 是运行期决定的。
    注意:为什么不能添加属性,原因就是category是运行期决定的,在运行期类的内存布局已经确定,如果添加实例变量会破坏类的内存布局,会产生意想不到的错误。但是,我们可以使用 runtimeobjc_setAssociatedObjectobjc_getAssociatedObject 给该属性动态绑定。

extension :扩展

  1. 可以给类添加成员变量,但是是私有的
  2. 可以給类添加方法,但是是私有的
  3. 添加的属性和方法是类的一部分,在编译期就决定的。在编译器和头文件的@interface和实现文件 @implement一起形成了一个完整的类。
  4. 伴随着类的产生而产生,也随着类的消失而消失
  5. 必须有类的源码才可以给类添加extension,所以对于系统一些类,如NSString,就无法添加类扩展
  6. 不能给 NSObject添加 Extension,因为在 extension 中添加的方法或属性必须在源类的文件的.m文件中实现才可以,即:你必须有一个类的源码才能添加一个类的 extension

7、消息转发机制,消息转发机制和其他语言的消息机制优劣对比?

消息转发机制:当接收者收到消息后,无法处理该消息时(即:找不到调用的方法SEL),就会启动消息转发机制,流程如下:

  1. 第一阶段:动态解析咨询接收者,询问它是否可以动态增加这个方法实现

  2. 第二阶段:在第一阶段中,接收者无法动态增加这个方法实现,那么将会进行快速转发,系统将询问是否有其他对象可能执行该方法,如果可以,系统将转发给这个对象处理。

  3. 第三阶段:在第二阶段中,如果没有其他对象可以处理,那么进行慢速转发,系统将该消息相关的细节封装成NSInvocation对象,再给接收者最后一次机会,如果这里仍然无法处理,接收者将收到doesNotRecognizeSelector方法调用,此时程序将 crash

// 第一阶段 咨询接收者是否可以动态添加方法
+ (BOOL)resolveInstanceMethod:(SEL)selector
+ (BOOL)resolveClassMethod:(SEL)selector //处理的是类方法

// 第二阶段:询问是否有其他对象可以处理
- (id)forwardingTargetForSelector:(SEL)selector

// 第三阶段
// 慢速转发 1.签名 2.转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
- (void)forwardInvocation:(NSInvocation *)invocation

// 无法识别该消息  crash
-(void)doesNotRecognizeSelector:(SEL)aSelector

8、在方法调用的时候,方法查询-> 动态解析-> 消息转发 之前做了什么 ?

OC中的方法调用,编译后的代码最终都会转成objc_msgSend(id , SEL, ...)方法进行调用,这个方法第一个参数是一个消息接收者对象。

  1. RunTime通过这个对象的isa指针找到这个对象的类对象

  2. 从类对象中的cache中查找是否存在SEL对应的IMP

  3. 若不存在,则会在 method_list中查找

  4. 如果还是没找到,则会到supper_class中查找

  5. 仍然没找到的话,就会调用_objc_msgForward(id, SEL, ...)进行消息转发

9、IMP、SEL、Method的区别和使用场景

  • IMP:是方法的实现,即:一段c函数

  • SEL:是方法名

  • Method:objc_method类型指针,它是一个结构体,如下:

struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
} 

使用场景:

实现类的swizzle的时候会用到,通过class_getInstanceMethod(class, SEL)来获取类的方法Method,其中用到了SEL作为方法名

调用method_exchangeImplementations(Method1, Method2)进行方法交换

我们还可以给类动态添加方法,此时我们需要调用class_addMethod(Class, SEL, IMP, types),该方法需要我们传递一个方法的实现函数IMP,例如:

static void funcName(id receiver, SEL cmd, 方法参数...) {
   // 方法具体的实现   
}

函数第一个参数:方法接收者,第二个参数:调用的方法名SEL,方法对应的参数,这个顺序是固定的。

10、load、initialize方法的区别什么?在继承关系中他们有什么区别?

load:当类被装载的时候被调用,只调用一次

调用方式并不是采用RunTimeobjc_msgSend方式调用的,而是直接采用函数的内存地址直接调用的多个类的load调用顺序,是依赖于compile sources中的文件顺序决定的,根据文件从上到下的顺序调用 ;子类和父类同时实现load的方法时。父类的方法先被调用,本类与category的调用顺序是,优先调用本类的(注意:category是在最后被装载的)。 多个category,每个load都会被调用(这也是load的调用方式不是采用objc_msgSend的方式调用的),同样按照compile sources中的顺序调用的 load是被动调用的,在类装载时调用的,不需要手动触发调用 注意:当存在继承关系的两个文件时,不管父类文件是否排在子类或其他文件的前面,都是优先调用父类的,然后调用子类的。

例如:compile sources中的文件顺序如下:SubB、SubA、A、B,load的调用顺序是:B、SubB、A、SubA。

分析:SubB是排在compile sources中的第一个,所以应当第一个被调用,但是SubB继承自B,所以按照优先调用父类的原则,B先被调用,然后是SubB,A、SubA。

第二种情况:compile sources中的文件顺序如下:B、SubA、SubB、A,load调用顺序是:B、A、SubA、SubB,这里我给大家画个图梳理一下:

image.png

initialize:类或子类第一次收到消息时被调用(即:静态方法或实例方法第一次被调用,也就是这个类第一次被用到的时候),只调用一次

  • 调用方式是通过RunTimeobjc_msgSend的方式调用的,此时所有的类都已经装载完毕

  • 子类和父类同时实现initialize,父类的先被调用,然后调用子类的

  • 本类与category同时实现initializecategory会覆盖本类的方法,只调用category

  • initialize一次(这也说明initialize的调用方式采用objc_msgSend的方式调用的)

  • initialize是主动调用的,只有当类第一次被用到的时候才会触发

11、说说消息转发机制的优劣?

优点:

  • 动态化更新方案

    (例如: JSPatch):消息转发机制来进行JS和OC的交互,从而实现iOS的热更新

  • 实现多重代理

    利用消息转发机制可以无代码侵入的实现多重代理,让不同对象可以同时代理同个回调,然后在各自负责的区域进行相应的处理,降低了代码的耦合程度。

  • 间接实现多继承

    OC本身不支持多继承,但是可以通过消息转发机制在内部创建多个功能的对象,把不能实现的功能给转发到其他对象上去,这样就做出来一种多继承的假象。转发和继承相似,可用于为OC编程添加一些多继承的效果,一个对象把消息转发出去,就好像他把另一个对象中放法接过来或者“继承”一样。消息转发弥补了objc不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。

  • 预防线上奔溃

    利用消息转发机制对消息进行转发和替换,预防线上版本奔溃

缺点:

  • 消耗性能(延长了消息发送的周期,提高了成本)

  • bug 的定位更加困难

12、iOS你在项目中用过 RunTime 吗?举个例子。

13、RunTime 是如何把 weak变量的自动置 nil 的?

RunTime对注册的类会进行布局,对于 weak 对象会放入一个 hash 表中。用 weak 对象指向的内存地址作为 key,当此对象引用计数为 0 时会 dealloac。假如 weak 对象的内存地址是 a,那么就会以 a 为键,在 hash 表中进行搜索,找出所有 a 对应的 weak 对象,从而置为 nil

weak 修饰的指针默认为 nil。(在 OC 中对 nil 发送消息是安全的)

14、objc中向一个 nil 对象发送消息将会发生什么?

如果向一个nil对象发送消息,首先在寻找对象的isa指针时就是0地址返回了,所以不会出现任何错误。也不会崩溃。
详解:

如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil);
如果方法返回值为指针类型,其指针大小为小于或者等于sizeof(void*) ,float,double,long double 或者long long的整型标量,发送给nil的消息将返回0;
如果方法返回值为结构体,发送给nil的消息将返回0。结构体中各个字段的值将都是0;
如果方法的返回值不是上述提到的几种情况,那么发送给nil的消息的返回值将是未定义的。

Block

1、block的内部实现,结构体是什么样的?

  • block和函数类似, 只不过是直接定义在另一个函数里的, 和定义它的那个函数共享同一个范围内的东西。block可以实现闭包, 有些人也称它作

  • 结构如下:

struct Block_descriptor {
	unsigned long int reserved;
	unsigned long int size;
	void (*copy)(void *dst, void *src);
	void (*dispose)(void *);
};

struct Block_layout {
	void *isa;
	int flags;
	int reserved; 
	void (*invoke)(void *, ...);
	struct Block_descriptor *descriptor;
	/* Imported variables. */
};
  • 由上图可知,block实际上是由6部分组成的:

    • isa 指针

    • flags,用于按bit位表示的一些block附加信息

    • reserved,保留变量

    • invoke,函数指针,指向具体的block实现的函数调用地址

    • descriptor,从它的结构体可以看出,主要表示该block的附加描述信息,主要是size大小,以及copydispose函数的指针

    • variables,捕获的变量,block能访问它的外部的局部变量,就是因为将这些变量(或变量地址)复制到了结构体中

2、block 是类吗?有哪些类型?

block 是类。 它有三种类型:分别是ARC下:__NSGlobalBlock____NSMallocBlock__,切换到非ARC下的__NSStackBlock__

  1. __NSGlobalBlock__ :全局静态block,不访问任何外部变量,isa 指向_NSConcreteGlobalBlock

    1.1. 这种块不会捕捉任何变量,运行时也无须有状态来参与。

    1.2. 全局块声明在全局内存里, 在编译期已经完全确定了

  2. __NSMallocBlock__ :保存在堆上的block,引用计数为0时销毁,isa指向_NSConcreteMallocBlock

    一个__NSStackBlock__类型block做调用copy,那会将这个block从栈复制到堆上,堆上的这个block类型就是__NSMallocBlock__所以__NSMallocBlock__类型的block是存储在堆区。如果对一个__NSMallocBlock__类型block做copy操作,那这个block的引用计数+1。

    ARC环境下,编译器会根据情况,自动将栈上的block复制到堆上。

  3. __NSStackBlock__ :保存在栈上的block,函数返回时销毁,isa指向_NSConcreteStackBlock

    如果一个block里面访问了普通的局部变量,那它就是一个__NSStackBlock__它在内存中存储在栈区,栈区的特点就是其释放不受开发者控制,都是由系统管理释放操作的,所以在调用__NSStackBlock__类型block时要注意,一定要确保它还没被释放。如果对一个__NSStackBlock__类型block做copy操作,那会将这个block从栈复制到堆上

3、一个int变量被 __block 修饰与否的区别?block 的变量截获?

  • 没有被__block修饰的intblock体中对这个变量的引用是值拷贝,在block中是不能被修改的

    通过__block修饰的intblock体中对这个变量的引用是指针拷贝它会生成一个结构体,复制这个变量的指针引用,从而达到可以修改变量的作用。

  • block的变量截获:

    • __block会将block体内引用外部变量的变量进行拷贝,将其拷贝到block的数据结构中,从而可以在block体内访问或修改外部变量。

    • 外部变量未被__block修饰时,block数据结构中捕获的是外部变量的值,通过__block修饰时,则捕获的是对外部变量的指针引用

    注意:block内部访问全局变量时,全局变量不会被捕获到block数据结构中。

4、block在修改NSMutableArray,需不需要添加__block

  • 如果修改的是NSMutableArray存储内容的话,是不需要添加__block修饰的。

  • 如果修改的是 NSMutableArray对象的本身,那必须添加__block修饰。 参考block变量捕获(第3点)

5、block怎么进行内存管理的?

  • block内部引用全局变量或者不引用任何外部变量时,该block是在全局内存中的。(全局静态block)

  • block内部引用了外部的非全局变量的时候:

    • 在MRC中,该block是在栈内存中的

    • 在ARC中,该block是在堆内存中的

    也就是说,ARC下只存在全局block堆block

    通过__block修饰的变量,在block内部依然会对其引用计数+1,可能会造成循环引用。

    通过__weak修饰的变量,在block内部不会对其引用计数+1,不会造成循环引用。

6、block可以用strong修饰吗?

  • MRC环境中,是不可以的strong修饰符会对修饰的变量进行retain操作,这样并不会将栈中的block拷贝到堆内存中,而执行的block是在堆内存中,所以用strong修饰的block会导致在执行的时候因为错误的内存地址,导致闪退

  • ARC环境中,是可以的。因为在ARC环境中的block只能在堆内存全局内存中,因此不涉及到从栈拷贝到堆中的操作。

7、解决循环引用时为什么要用__strong__weak修饰?

  • __weak修饰的变量,不会出现引用计数+1,也就不会造成block强持有外部变量,这样也就不会出现循环引用的问题了。

  • 但是,我们的block内部执行的代码中,有可能是一个异步操作,或者延迟操作。此时引用的外部变量可能会变成nil,导致意想不到的问题,而我们在block内部通过__strong修饰这个变量时,block会在执行过程中强持有这个变量,此时这个变量也就不会出现nil的情况,当block执行完成后,这个变量也就会随之释放了。

  • 那么问题来了: Masonry 需要用 __weak 修饰吗?如果不用,那为什么呢?
    Masonry 内部并没有使用 __weak , 在 makeConstraintsupdateConstraints 中 View 并没有持有 Block ,所以这个 block 只是一个 栈block 。当执行完 block(constraintMaker) 就出栈释放掉了,所以不会造成循环引用。

8、block 发生copy 的时机?

一般情况在ARC环境中,编译器将创建在栈中的block会自动拷贝到堆内存中,而block作为方法函数的参数传递时,编译器不会做copy操作。

  • block作为方法或函数的返回值时,编译器会自动完成copy操作。

  • block赋值给通过strongcopy修饰的idblock类型的成员变量时。

  • block 作为参数被传入方法名带有 usingBlockCocoa Framework 方法或 GCDAPI 时。

9、block访问对象类型的auto变量时,在ARCMRC下有什么区别?

首先我们知道,在ARC下,栈区创建的block会自动copy到堆区;而MRC下,就不会自动拷贝了,需要我们手动调用copy函数。

我们再说说blockcopy操作,当block从栈区copy到堆区的过程中,也会对block内部访问的外部变量进行处理,它会调用Block_object_assign函数对变量进行处理,根据外部变量是strong还会weakblock内部捕获的变量进行引用计数+1或-1,从而达到强引用或弱引用的作用

因此

ARC下,由于block被自动copy到了堆区,从而对外部的对象进行强引用,如果这个对象同样强引用这个block,就会形成循环引用。

MRC下,由于访问的外部变量是auto修饰的,所以这个block属于栈区的,如果不对block手动进行copy操作,在运行完block的定义代码段后,block就会被释放,而由于没有进行copy操作,所以这个变量也不会经过Block_object_assign处理,也就不会对变量强引用。

简单说就是:

ARC下会对这个对象强引用,MRC下不会。

多线程

1、什么是进程?什么是线程?进程和线程的关系?什么是多进程?什么是多线程?

  • 进程:

    • 进程是一个具有独立功能的程序关于某次数据集合的一次运行活动,他是操作系统分配资源的基本单位

    • 进程是指系统正在运行中的一个应用程序,就是一段程序执行的过程。我们可以理解为手机上的一个app。

    • 每个进程之间是独立的。每个进程均运行在起专用且受保护的内存空间内,拥有独立运行所需的全部资源。

    • 进程是操作系统进行资源分配的单位

  • 线程:

    • 程序执行流的最小单元,线程是进程中的一个实体

    • 一个进程想要执行任务,必须至少有一条线程。应用程序启动的时候,系统会默认开启一条线程,也就是主线程。

  • 进程和线程的关系:

    • 线程是进程的执行单元,进程的所有任务都在线程中执行。

    • 线程是CPU分配资源和调度的最小单位

    • 一个程序可对应多个进程(多进程);一个进程中可对应多个线程,但至少要有一条线程。

    • 同个进程内的线程共享进程资源。

  • 多进程:

    • 进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。显然程序是死的(静态的),进程是活动的(动态的)。

    • 进程可以分为系统进程用户进程

      • 系统进程:凡是用于完成操作系统的各种功能的进程就是系统进程,他们就是出于运行状态下的操作系统本身

      • 用户进程:运行用户程序时创建的运行在用户态下的进程。

    • 进程又被细化为线程,也就是一个进程下有多个能独立运行的更小的单位

    • 在同一个时间里,同一个操作系统中如果允许两个或两个以上的进程处于运行状态,这便是多进程

  • 多线程:

    • 同一时间,CPU 只能处理1条线程,只有1条线程执行。多线程并发执行,其实是CPU快速地在多条线程之间调度(切换)。如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象

    • 如果线程非常至多(N条)CPU会在这些(N条)线程之间调度,消耗大量的CPU资源,每条线程被调用执行的频率会降低(线程的执行效率降低)。

    • 多线程的优点:

      • 适当提高程序的执行效率

      • 适当提高资源的利用率(CPU、内存利用率)

    • 多线程的缺点:

      • 开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512kb),如若开启大量线程,会占用大量的内存空间,就会降低程序的性能

      • 线程越多,CPU在调度线程的开销就越大

      • 程序设计更加复杂:如线程之间的通信、多线程之间的数据共享等

2、iOS开发中有多少类型的线程?分别对比?

  1. NSThread 每个NSThread对象对应一个线程,量级较轻(真正的多线程)。是对pthread(其是POSIX线程的API,是C语言的技术,当然它可以直接操作线程)的抽象。
  2. NSOperation/NSOperationQueue 面向对象的线程技术,是对GCD的抽象,容易理解和使用。
  3. GCD —— Grand Central Dispatch(派发) 是基于C语言的框架,可以充分利用多核,是苹果推荐使用的多线程技术

对比:

线程类型优点缺点
NSThread1. 跨平台C语言标准库中的多线程框架 2. 使用简单1. 过于底层使用很麻烦,需要封装使用。 2. 需要自己来管理线程的生命周期线程同步加锁睡眠唤醒。过程不可避免的有一定的系统“开销”
NSOperation / NSOperationQueue1. 更加面向对象,可以设置并发数量,可以设置优先级 可以设置依赖,可以任务执行状态控制:isReady(是否准备好执行),isExecuting(是否正在执行),isFinished(是否执行完毕),isCancelled(是否被取消) 2.用关心线程的管理和数据的同步,把精力放在自己需要执行的任务或操作上就行了 3. GCD 的封装用于相对复杂的场景,相对简单的官方推荐 GCD
GCD(Grand Central Dispatch)1. iOS5后苹果推出的双核CPU优化的多线程框架,iOS 4.0 才能使用,是代替上面两个技术的高效而且强大的技术 2. 它基于block的特性导致它能极为简单的在不同代码作用域之间传递上下文,效率高 3. GCD自动根据系统负载来增减线程数量,这就减少了上下文的切换和提高了计算效率 4. 安全,无需加锁或其他同步机制 4. 它是基于C语言的1. 不能设置并发数,需要写一些代码曲线方式实现并发 2. 不能设置优先级

3、GCD有哪些队列,默认提供哪些队列?

3中队列:主线程队列、并发队列、串行队列

在GCD中有两种队列:串行队列并发队列。两者都符合 FIFO 的原则,二者的主要区别是:执行的顺序不同开启的线程数不同

  1. 主线程队列: main queue可以调用dispatch_get_main_queue()来获得。因为main queue是与主线程相关的,所以这是一个串行队列。和其它串行队列一样,这个队列中的任务一次只能执行一个。它能保证所有的任务都在主线程执行,而主线程是唯一可用于更新 UI 的线程。

  2. 串行队列(Serial Dispatch Queue):

    同一时间内,队列中只能执行一个任务,只有当前的任务执行完成之后,才能执行下一个任务。(只能开启一个线程,一个线程执行完毕后,再执行下一个任务)。主队列是主线程上的一个串行队列,是系统自动为程序创建的。

  3. 并行队列(Concurrent Dispatch Queue):

    同时允许多个任务同时执行。(可以开启多个线程,并且同时执行)。并发队列的并发功能只有在异步(dispatch_async) 函数下才有效

4、GCD有哪些方法 api?

  1. Dispatch Queue :

    开发者要做的只是定义想执行的任务并追加到适当的 Dispatch Queue 中。

   dispatch_async { queue,  ^{
            //想执行的任务
    });

通过 dispatch_async 函数“追加”赋值在变量 queue 的“Dispatch Queue中”。
Dispatch Queue 的种类:
有两种Dispatch Queue,一种是等待现在执行中处理的 Serial Dispatch Queue,另一种是不等待现在执行中处理的 Concurrent Dispatch Queue

  1. dispatch_queue_create :

    创建队列

  2. Main Dispatch QueueGlobal Dispatch Queue

    系统提供的两种队列

  3. dispatch_set_target_queue :

    变更队列执行的优先级

  4. dispatch_after :

    延时执行

    注意的是dispatch_after函数并不是在指定时间后执行处理,而只是在指定时间追加处理到 Dispatch Queue

  5. dispatch_group :

    调度任务组

    • dispatch_group_notify:最后任务执行完的通知,比如:

      - (void)dispatch_group {
      
          dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT , 0);
          dispatch_group_t group = dispatch_group_create();
      
          dispatch_group_async(group, queue, ^{
              NSLog(@"thread1:%@", [NSThread currentThread]);
          });
      
          dispatch_group_async(group, queue, ^{
              NSLog(@"thread2:%@", [NSThread currentThread]);
          });
      
          dispatch_group_async(group, queue, ^{
              NSLog(@"thread3:%@", [NSThread currentThread]);
          });
      
        	// 三个异步执行结束后,dispatch_group_notify 得到通知
          dispatch_group_notify(group, dispatch_get_main_queue(), ^{ // 4
              NSLog(@"completed:%@", [NSThread currentThread]);
          });
      }
      
    • dispatch_group_wait

      dispatch_group_wait实际上会使当前的线程处于等待的状态,也就是说如果是在主线程执行dispatch_group_wait,在上面的block执行完之前,主线程会处于卡死的状态。可以注意到dispatch_group_wait第二个参数是指定超时的时间,如果指定为DISPATCH_TIME_FOREVER(如上面这个例子)则表示会永久等待,直到上面的Block全部执行完。除此之外,还可以指定为具体的等待时间,根据dispatch_group_wait的返回值来判断是上面block执行完了还是等待超时了。

      func testGroup3() -> void {
      	let globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
        let group = dispatch_group_create()
      
        dispatch_group_async(group, globalQueue) { () -> Void in
            println("1")
        }
      
      	dispatch_group_async(group, globalQueue) { () -> Void in
          	println("2")
      	}
      
      	dispatch_group_async(group, globalQueue) { () -> Void in
            println("3")
      	}
      
      	//使用dispatch_group_wait函数
      	dispatch_group_wait(group, DISPATCH_TIME_FOREVER)
      	println("completed")
      }
      
    • dispatch_barrier_async

      dispatch_barrier_async就如同它的名字一样,在队列执行的任务中增加“栅栏”,在增加“栅栏”之前已经开始执行的block将会继续执行,当dispatch_barrier_async开始执行的时候其他的block处于等待状态,dispatch_barrier_async的任务执行完后,其后的block才会执行。

  6. dispatch_syncdispatch_async

    • dispatch_sync : 把任务Block同步追加到指定的Dispatch Queue

    • dispatch_async :把任务Block异步追加到指定的Dispatch Queue

  7. dispatch_apply

    dispatch_apply会将一个指定的block执行指定的次数。如果要对某个数组中的所有元素执行同样的block的时候,这个函数就显得很有用了,用法很简单,指定执行的次数以及Dispatch Queue,在block回调中会带一个索引,然后就可以根据这个索引来判断当前是对哪个元素进行操作:

    func testGroup3() -> void {
    
    	let globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
      dispatch_apply(10, globalQueue) { (index) -> Void in
        print(index)
      }
    
      print("completed") 
    }
    

    由于是Concurrent Dispatch Queue不能保证哪个索引的元素是先执行的,但是“completed”一定是在最后打印,因为dispatch_apply函数是同步的,执行过程中会使线程在此处等待,所以一般的,我们应该在一个异步线程里使用dispatch_apply函数:

    func testGroup3() -> void {
    
    	let globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
      dispatch_async(globalQueue, { () -> Void in
        dispatch_apply(10, globalQueue) { (index) -> Void in
    			print(index)
        }
        print("completed")
      })
    
      print("在dispatch_apply之前") 
    }
    
  8. dispatch_suspend / dispatch_resume

    某些情况下,我们可能会想让Dispatch Queue暂时停止一下,然后在某个时刻恢复处理,这时就可以使用dispatch_suspend以及dispatch_resume函数:

    func testGroup3() -> void {
    
    	let globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
      //暂停
      dispatch_suspend(globalQueue)
    
      //恢复
      dispatch_resume(globalQueue)
    }
    

    注意: 暂停时,如果已经有block正在执行,那么不会对该block的执行产生影响。dispatch_suspend只会对还未开始执行的block产生影响。

  9. Dispatch Semaphore

信号量在多线程开发中被广泛使用,当一个线程在进入一段关键代码之前,线程必须获取一个信号量,一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待前面的线程释放信号量
信号量的具体做法是:当信号计数大于0时,每条进来的线程使计数减1,直到变为0,变为0后其他的线程将进不来,处于等待状态;执行完任务的线程释放信号,使计数加1,如此循环下去。

下面这个例子中使用了10条线程,但是同时只执行一条,其他的线程处于等待状态:

func testGroup3() -> void {
	let globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
	let semaphore = dispatch_semaphore_create(1)

	for i in 0 ... 9 {
	
		dispatch_async(globalQueue, { () -> Void in
			dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
		    let time = dispatch_time(DISPATCH_TIME_NOW, (Int64)(2 * NSEC_PER_SEC))
		
		    dispatch_after(time, globalQueue) { () -> Void in
			    print("2秒后执行")
		        dispatch_semaphore_signal(semaphore)
		    }
		})
	}
}

取得信号量的线程在2秒后释放了信息量,相当于是每2秒执行一次。
通过上面的例子可以看到,在GCD中,用dispatch_semaphore_create函数能初始化一个信号量,同时需要指定信号量的初始值;使用dispatch_semaphore_wait函数分配信号量并使计数减1,为0时处于等待状态;使用dispatch_semaphore_signal函数释放信号量,并使计数加1。
另外dispatch_semaphore_wait同样也支持超时,只需要给其第二个参数指定超时的时候即可,同Dispatch Groupdispatch_group_wait函数类似,可以通过返回值来判断。
注意:如果是在OS X 10.8或iOS 6以及之后版本中使用,Dispatch Semaphore将会由ARC自动管理,如果是在此之前的版本,需要自己手动释放。

  1. dispatch_once

dispatch_once函数通常用在单例模式上,它可以保证在程序运行期间某段代码只执行一次。如果我们要通过dispatch_once创建一个单例类,在Swift可以这样:

 class SingletonObject {
      class var sharedInstance : SingletonObject {
         struct Static {
           static var onceToken : dispatch_once_t = 0
           static var instance : SingletonObject? = nil
         }

         dispatch_once(&Static.onceToken) {
           Static.instance = SingletonObject()
         }
         return Static.instance!
      }
    }

这样就能通过GCD的安全机制保证这段代码只执行一次。

5、GCD主线程 & 主队列的关系?

提交到主队列的任务在主线程执行。

  1. 主队列是主线中的一个串行队列

  2. 所有的和UI相关的操作(刷新或者点击按钮)都必须在主线程中的主队列中去执行,否则无法更新UI

  3. 每一个应用程序只有唯一的一个主队列用来update UI

    补充一点:如果在主线程中创建自定义队列(串行或者并行均可),在这个队列中执行同步任务,同样可以更新UI操作,主队列中可以更新UI,自定义队列也可以更新UI,但自定义队列的更新UI的前提是在主线程中执行同步任务。

6、如何实现同步?有多少方式就说多少

  • dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block) 在某队列开启同步线程

  • dispatch_barrier_sync() 障碍锁的方式同步

  • dispatch_group_create() + dispatch_group_wait()

  • dispatch_apply() 插队追加 操作同步

  • dispatch_semaphore_create() + dispatch_semaphore_wait() 信号量锁

  • 串行NSOperationQueue队列并发数为1的时候 [NSOpertaion start] 启动任务即使同步操作 (NSOperationQueue.maxConcurrentOperationCount = 1)

  • pthread_mutex底层锁函数

  • 上层应用层封装的NSLock

  • NSRecursiveLock 递归锁,这个锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中

  • NSConditionLock & NSCondition 条件锁

  • @synchronized 同步操作 单位时间内只允许一个线程进入临界区

  • dispatch_once() 单位时间内只允许一个线程进入临界区

7、dispatch_once 实现原理 ?

这个问题问的很傻吊也很高超.因为要解释清楚所有步骤需要记住里面所有代码

我认为这个问题应该从操作系统层面回答, 这个问题的核心是操作系统返回状态决定的,单位时间内操作系统只允许一个线程进入临界区,进入临界区的线程会被标记

回归到代码就是

dispatch_once(dispatch_once_t *val, dispatch_block_t block)  
    |_____dispatch_once_f(val, block, _dispatch_Block_invoke(block))  
        |_______&l->dgo_once  // &l->dgo_once 地址中存储的值。显然若该值为DLOCK_ONCE_DONE,即为once已经执行过

dgo_oncedispatch_once_gate_s的成员变量

typedef struct dispatch_once_gate_s {
    union {
        dispatch_gate_s dgo_gate;
        uintptr_t dgo_once;
    };
} dispatch_once_gate_s, *dispatch_once_gate_t;

有个内联函数static inline bool _dispatch_once_gate_tryenter(dispatch_once_gate_t l)

这个内联函数返回一个 原子性操作的结果

return os_atomic_cmpxchg(&l->dgo_once, DLOCK_ONCE_UNLOCKED,(uintptr_t)_dispatch_lock_value_for_self(), relaxed)

比较+交换 的原子操作。比较 &l->dgo_once 的值是否等于 DLOCK_ONCE_UNLOCKED

这样就实现了我们的执行1次的GCD API.

8、什么情况下会死锁?死锁的应对策略有哪些?怎么避免死锁?

  • 死锁发生的四个必要条件是:
  1. 互斥条件(Mutual exclusion) :

    资源不能被共享,只能由一个进程使用

  2. 请求与保持条件(Hold and wait):

    进程已获得了一些资源,但因请求其它资源被阻塞时,对已获得的资源保持不放。

  3. 不可抢占条件(No pre-emption) :

    有些系统资源是不可抢占的,当某个进程已获得这种资源后,系统不能强行收回,只能由进程使用完时自己释放。

  4. 循环等待条件(Circular wait) :

    若干个进程形成环形链,每个都占用对方申请的下一个资源。

  • 一般死锁的应对策略有:
  1. 死锁预防:

    破坏导致死锁必要条件中的任意一个就可以预防死锁。例如,要求用户申请资源时一次性申请所需要的全部资源,这就破坏了保持和等待条件;将资源分层,得到上一层资源后,才能够申请下一层资源,它破坏了环路等待条件。预防通常会降低系统的效率。

  2. 死锁避免:

    避免是指进程在每次申请资源时判断这些操作是否安全。例如,使用银行家算法。死锁避免算法的执行会增加系统的开销。

  3. 死锁检测:

    死锁预防和避免都是事前措施,而死锁的检测则是判断系统是否处于死锁状态,如果是,则执行死锁解除策略。

  4. 死锁解除:

    这是与死锁检测结合使用的,它使用的方式就是剥夺。即:将某进程所拥有的资源强行收回,分配给其他的进程。

  • 死锁的避免:

    • 死锁的预防是通过破坏产生条件来阻止死锁的产生,但这种方法破坏了系统的并行性和并发性。

    • 死锁产生的前三个条件是死锁产生的必要条件,也就是说要产生死锁必须具备的条件,而不是存在这3个条件就一定产生死锁,那么只要在逻辑上回避了第四个条件就可以避免死锁

    • 避免死锁采用的是允许前三个条件存在,但通过合理的资源分配算法来确保永远不会形成环形等待的封闭进程链,从而避免死锁。该方法支持多个进程的并行执行,为了避免死锁,系统动态的确定是否分配一个资源给请求的进程。方法如下:

      • 如果一个进程的当前请求的资源会导致死锁,系统拒绝启动该进程;

      • 如果一个资源的分配会导致下一步的死锁,系统就拒绝本次的分配;

        显然要避免死锁,必须事先知道系统拥有的资源数量及其属性。

9、有哪些类型的线程锁?分别介绍下作用和使用场景?

锁类型使用场景备注
pthread_mutex互斥锁PTHREAD_MUTEX_NORMAL#import <pthread.h>
OSSpinLock自旋锁不安全,iOS 10 已启用
os_unfair_lock互斥锁替代 OSSpinLock
pthread_mutex(recursive)递归锁PTHREAD_MUTEX_RECURSIVE,#import <pthread.h>
pthread_cond_t条件锁#import <pthread.h>
pthread_rwlock读写锁读操作重入,写操作互斥
@synchronized互斥锁性能差,且无法锁住内存地址更改的对象
NSLock互斥锁封装 pthread_mutex
NSRecursiveLock递归锁封装pthread_mutex(recursive)
NSCondition条件锁封装 pthread_cond_t
NSConditionLock条件锁可以指定具体条件值 封装 pthread_cond_t

13、iOS各种锁的性能,琐是毫秒级别还是微妙级别?

  • 琐是 ns 纳秒 us微秒级别

  • 参考自YY大神的不再安全的 OSSpinLock。单位是 ns 纳秒。
    锁耗时

  • 锁相关的概念定义:

    1. 临界区:
      指的是一块对公共资源进行访问的代码,并非一种机制或是算法。
      每个进程中访问临界资源的那段程序称为临界区,每次只允许一个进程进入临界区,进入后不允许其他进程进入。

    2. 自旋锁:
      是用于多线程同步的一种锁,线程反复检查锁变量是否可用。
      a、 由于线程在这一过程中保持执行,因此是一种忙等待
      b、 一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。
      c、 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。

    3. 互斥锁(Mutex):
      用于保护临界区,确保同一时间只有一个线程访问数据。 对共享资源的访问,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。
      a、 互斥锁加锁失败而阻塞是由操作系统内核实现的,当加锁失败后,内核将线程置为睡眠状态;等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程加锁成功后就可以继续执行。
      b、 性能开销成本,两次线程上下文切换的成本。
      当线程加锁失败时,内核将线程的状态从【运行】切换到睡眠状态,然后把CPU切换给其他线程运行;
      当锁被释放时,之前睡眠状态的线程会变成就绪状态,然后内核就会在合适的时间把CPU切换给该线程运行

    4. 读写锁:
      是计算机程序的并发控制的一种同步机制,也称“共享-互斥锁”、多读者-单写者锁。用于解决多线程对公共资源读写问题。读操作可并发重入,写操作是互斥的。 读写锁通常用互斥锁、条件变量、信号量实现。

    5. 信号量(semaphore):
      是一种更高级的同步机制,互斥锁可以说是 semaphore 在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。

    6. 条件锁:
      就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行。

    7. 递归锁(Recursive Lock):
      也称为重入互斥锁(reentrant mutex),是互斥锁的一种,同一线程对其多次加锁不会产生死锁。 递归锁会使用引用计数机制,以便可以从同一线程多次加锁、解锁,当加锁、解锁次数相等时,锁才可以被其他线程获取。

11、NSOperationQueue 中的 maxConcurrentOperationCount 默认值

默认值 -1。 这个值操作系统会根据资源使用的综合开销情况设置。

12、NSTimerCADisplayLinkdispatch_source_t 的优劣?

定时器类型优势劣势
NSTimer使用简单依赖 RunLoop,具体表现在无 RunLoop 无法使用、NSRunLoopCommonModes、不精确
CADisplayLink依赖屏幕刷新频率出发事件,最精.最合适做UI刷新若屏幕刷新被影响,事件也被影响、事件触发的时间间隔只能是屏幕刷新 duration 的倍数、若事件所需时间大于触发事件,跳过数次、不能被继承
dispatch_source_t不依赖 RunLoop依赖线程队列,使用麻烦 使用不当容易Crash

13、多线程可以访问同一个对象吗 ?多进程呢?

  • 多线程可以访问同一个对象可分为3种情况处理:

    1. 如果只是只读,不用加锁。
    2. 如果只写的话,需要加锁。
    3. 如果需要读且写的话,需要加锁(读写锁满足)。
      使用读写锁 pthread_rwlock。获取一个读写锁用于读称为共享锁,获取一个读写锁用于写称为独占锁,因此这种对于某个给定资源的共享访问也称为共享-独占上锁
  • 多进程访问同一个对象
    一个程序几个进程在于这个程序的开发者的设置,可以是1个,也可以是多个的。

    1. 一个程序里有很多个进程
      一个程序几个进程在于这个程序的开发者的设置,可以是1个,也可以是多个的。一个应用程序,启动多个处理进程。换言之,所有进程隶属于当前应用程序;这是所谓的多进程服务
    2. 一个程序只有一个进程但被开启很多个
      启动多个同一应用程序,每个应用程序都是单进程。这个场景有些应用程序会禁用掉,有些是可以的,看应用程序的定位。如果允许,那么需要解决数据共享的问题(主要是数据写入);如果不允许,那么只能启动一个此类应用程序。
    • 所以 多个进程竞争,进程就会一直等待下去,形成死锁。

    • 所以 我们就可以根据死锁的四个必要条件互斥条件、请求与保持条件、不可抢占条件、不可剥夺条件), 使用死锁的四个应对策略死锁预防、死锁避免、死锁检测、死锁解除)来解决死锁问题。

    • 所以 我们也可以通过一些处理避免死锁

      1. 死锁的预防是通过破坏产生条件来阻止死锁的产生,但这种方法破坏了系统的并行性和并发性。

      2. 死锁产生的前三个条件是死锁产生的必要条件,而不是存在这3个条件就一定产生死锁,那么只要在逻辑上回避了第四个条件就可以避免死锁

      3. 避免死锁采用的是允许前三个条件存在,但通过合理的资源分配算法来确保永远不会形成环形等待的封闭进程链,从而避免死锁。该方法支持多个进程的并行执行,为了避免死锁,系统动态的确定是否分配一个资源给请求的进程。方法如下:

        1. 如果一个进程的当前请求的资源会导致死锁,系统拒绝启动该进程
        2. 如果一个资源的分配会导致下一步的死锁,系统就拒绝本次的分配

        总而言之:要避免死锁,必须事先知道系统拥有的资源数量及其属性。

优化

1、TableView 有什么好的性能优化方案?

  • Tableview 懒加载、Cell 复用
  • 高度缓存(因为 heightForRowAtIndexPath: 是调用最频繁的方法)

    • 当 cell 的行高固定时,使用固定行高 self.tableView.rowHeight = xxx;

    • 当 cell 的行高是不固定时,根据内容进行计算后缓存起来使用。第一次肯定会计算,后续使用缓存时就避免了多次计算;高度的计算方法通常写在自定义的cell中,调用时,既可以在设置 cell 高的代理方法中使用,也可以自定义的 model 中使用(且使用时,使用get方法处理)。

  • 数据处理

    • 使用正确的数据结构来存储数据;

    • 数据尽量采用局部的 section,或 cellRow 的刷新,避免 reloadData;

    • 大量数据操作时,使用异步子线程处理,避免主线程中直接操作;

    • 缓存请求结果。

  • 异步加载图片:SDWebImage 的使用

    • 使用异步子线程处理,然后再返回主线程操作

    • 图片缓存处理,避免多次处理操作

    • 图片圆角处理时,设置 layer 的 shouldRasterize 属性为 YES,可以将负载转移给 CPU

  • 按需加载内容

    • 滑动操作时,只显示目标范围内的 Cell 内容,显示过的超出目标范围内之后则进行清除;

    • 滑动过程中,不加载显示图片,停止时才加载显示图片。

  • 视图层面
    (1)减少 subviews 的数量,自定义的子视图可以整合在形成一个整体的就整合成一个整体的子视图;
    (2)使用 drawRect 进行绘制(即将 GPU 的部分渲染转接给 CPU ),或 CALayer 进行文本或图片的绘制。在实现 drawRect 方法的时候注意减少多余的绘制操作,它的参数 rect 就是我们需要绘制的区域,在 rect 范围之外的区域我们不需要进行绘制,否则会消耗相当大的资源;
    (3)异步绘制,且设置属性 self.layer.drawsAsynchronously = YES;(遇到复杂界面,遇到性能瓶颈时,可能就是突破口);
    (4)定义一种(尽量少)类型的 Cell 及善用 hidden 隐藏(显示) subviews;
    (5)尽量使所有的 view 的 opaque 属性为 YES,包括 cell 自身,以提高视图渲染速度(避免无用的 alpha 通道合成,降低 GPU 负载);
    (6)避免渐变,图片缩放的操作
    (7)使用 shadowPath 来画阴影;
    (8)尽量不使用 cellForRowAtIndexPath: ,如果你需要用到它,只用一次然后缓存结果;
    (9)cellForRowAtIndexPath 不要做耗时操作:如不读取文件 / 写入文件;尽量少用 addView 给 Cell 动态添加 View,可以初始化时就添加,然后通过 hide 来控制是否显示;
    (10)我们在 Cell 上添加系统控件的时候,实际上系统都会调用底层的接口进行绘制,大量添加控件时,会消耗很大的资源并且也会影响渲染的性能。当使用默认的 UITableViewCell 并且在它的 ContentView 上面添加控件时会相当消耗性能。所以目前最佳的方法还是继承 UITableViewCell,并重写 drawRect 方法
    (11)当我们需要圆角效果时,可以使用一张中间透明图片蒙上去使用 ShadowPath 指定 layer 阴影效果路径使用异步进行 layer 渲染(Facebook 开源的异步绘制框架 AsyncDisplayKit )设置 layer 的 opaque 值为 YES减少复杂图层合成尽量使用不包含透明(alpha)通道的图片资源尽量设置 layer 的大小值为整形值直接让美工把图片切成圆角进行显示,这是效率最高的一种方案很多情况下用户上传图片进行显示,可以让服务端处理圆角使用代码手动生成圆角 Image 设置到要显示的 View 上,利用 UIBezierPath ( CoreGraphics 框架)画出来圆角图片

2、界面卡顿和检测你都是怎么处理?

卡顿原因: 在一个VSyncGPUCPU的协作,未能将渲染任务完成放入到帧缓冲区,视频控制器去缓冲区拿数据的时候是空的,所以卡帧。

卡顿优化:

  • 图片等大文件IO缓存

  • 耗时操作放入子线程

  • 高代码执行效率(JSON to Model的方案,锁的使用等,减少循环,UI布局frame子线程预计算)

  • UI减少全局刷新尽量使用局部刷新

监控卡帧:

  • CADisplayLink 监控,结合子线程和信号量,两次事件触发时间间隔超过一个VSync的时长,上报调用栈

  • RunLoop中添加监听,如果kCFRunLoopBeforeSourceskCFRunLoopBeforeWaiting中间的耗时超过VSync的时间,那么就是卡帧了,然后这个时候拿到线程调用栈,看看。那个部分耗时长即可。

3、谈谈你对离屏渲染的理解?

离屏渲染(Off-Screen Rendering):分为CPU离屏渲染GPU离屏渲染两种形式。GPU离屏渲染指的是在当前屏幕缓冲区外新开辟一个缓冲区进行渲染操作

一般情况下,OpenGL会将应用提交到 Reader Server 的动画直接渲染显示,但对于一些复杂的图像动画显示并不能直接渲染叠加显示,而是需要根据 Command Buffer 分通道进行渲染之后在组合,这一组合过程中,就有些渲染通道是不会直接显示的;Masking 渲染需要更多的渲染通道和合并的步骤;而这些没有直接显示在屏幕上的通道就是 Off-Screen Readering Pass

  • 如何检查离屏渲染?
    通过勾选Xcode的Debug->View Debugging–>Rendering->Run->Color Offscreen-Rendered Yellow项。
  • 离屏渲染(Off-Screen Rendering)为什么会卡顿?
    离屏渲染需要个更多的渲染通道,而不同渲染通道间切换需要耗费一定的时间,这个时间内GPU会闲置,当通道达到一定数量对性能也会有较大的影响。
  • 离屏渲染的代价是很高的,主要体现在?
    1. 创建新缓冲区。
      要想进行离屏渲染,首先要创建一个新的缓冲区。
    2. 上下文切换。
      离屏渲染的整个过程,需要多次切换上下文环境:先是从当前屏幕(On-Screen)切换到离屏(Off-Screen),等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上有需要将上下文环境从离屏切换到当前屏幕。而上下文环境的切换是要付出很大代价的。
  • 情况或操作会引发离屏渲染?
    1. 为图层设置遮罩(layer.mask)
    2. 将图层的 layer.masksToBounds / view.clipsToBounds 属性设置为 true
    3. 将图层layer.allowsGroupOpacity 属性设置为YESlayer.opacity小于1.0
    4. 为图层设置阴影(layer.shadow)
    5. 为图层设置 layer.shouldRasterize = true
    6. 具有 layer.cornerRadius,layer.edgeAntialiasingMask,layer.allowsEdgeAntialiasing 的图层
    7. 使用CGContextdrawRect : 方法中绘制大部分情况下会导致离屏渲染,甚至仅仅是一个空的实现
  • 离屏渲染的优化方案 ?
    1. 圆角优化 :
      1.1、 使用 UIBezierPathCore Graphics 代替 layer 设置圆角。即:
      UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100,100,100,100)];
      imageView.image = [UIImage imageNamed:@"myImg"];
      
      //开始对imageView进行画图
      UIGraphicsBeginImageContextWithOptions(imageView.bounds.size,NO,1.0);
      //使用贝塞尔曲线画出一个圆形图
      [[UIBezierPath bezierPathWithRoundedRect:imageView.boundscornerRadius:imageView.frame.size.width]addClip];
      [imageView drawRect:imageView.bounds];
      imageView.image=UIGraphicsGetImageFromCurrentImageContext();
      //结束画图
      UIGraphicsEndImageContext();
      [self.view addSubview:imageView];
      
      1.2、使用 CAShapeLayerUIBezierPath 代替 layer 设置圆角。即:
      UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];
      imageView.image = [UIImage imageNamed:@"myImg"];
      
      UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:imageView.bounds.size];
      CAShapeLayer *maskLayer = [[CAShapeLayer alloc]init];
      
      //设置大小
      maskLayer.frame = imageView.bounds;
      //设置图形样子
      maskLayer.path = maskPath.CGPath;
      imageView.layer.mask = maskLayer;
      [self.view addSubview:imageView];
      
    2. Shadow 优化
      对于shadow,如果图层是个简单的几何图形或者圆角图形,我们可以通过设置shadowPath来优化性能,能大幅提高性能。示例如下:
      mageView.layer.shadowColor = [UIColorgrayColor].CGColor;
      imageView.layer.shadowOpacity = 1.0;
      imageView.layer.shadowRadius = 2.0;
      UIBezierPath *path = [UIBezierPath bezierPathWithRect:imageView.frame];
      imageView.layer.shadowPath = path.CGPath;
      
      我们还可以通过设置shouldRasterize属性值为YES来强制开启离屏渲染。其实就是光栅化(Rasterization)。既然离屏渲染这么不好,为什么我们还要强制开启呢?当一个图像混合了多个图层,每次移动时,每一帧都要重新合成这些图层,十分消耗性能。当我们开启光栅化后,会在首次产生一个位图缓存,当再次使用时候就会复用这个缓存。但是如果图层发生改变的时候就会重新产生位图缓存。所以这个功能一般不能用于UITableViewCell中,cell的复用反而降低了性能。最好用于图层较多的静态内容的图形。而且产生的位图缓存的大小是有限制的,一般是2.5个屏幕尺寸。在100ms之内不使用这个缓存,缓存也会被删除。所以我们要根据使用场景而定。
    3. 其他的一些优化建议
      3.1、当我们需要圆角效果时,可以使用一张中间透明图片蒙上去
      3.2、使用ShadowPath指定layer阴影效果路径
      3.3、使用异步进行layer渲染(Facebook开源的异步绘制框架AsyncDisplayKit (Texttrue)
      3.4、设置layer的opaque值为YES,减少复杂图层合成
      3.5、尽量使用不包含透明(alpha)通道的图片资源
      3.6、尽量设置layer的大小值为整形值
      3.7、直接让美工把图片切成圆角进行显示,这是效率最高的一种方案
      3.8、很多情况下用户上传图片进行显示,可以让服务端处理圆角、
      3.9、使用代码手动生成圆角Image设置到要显示的View上,利用UIBezierPath(CoreGraphics框架)画出来圆角图片

4、如何降低APP包的大小?

  • 资源优化:
    • 删除无用图片
      使用 LSUnusedResources 查找无用图片。注意 [UIImage imageNamed:[NSString stringWithFormat:“icon_%d.png”,index]]; 这种使用图片的方式,可能会被误删。
    • 删除重复资源:Json、Plist、Extension 等
    • 压缩图片资源
      • 使用 ImageOptim 无损压缩图片。
      • 使用 TinyPNG 有损压缩图片。使用的时候直接执行 tinypng *.png -k token 脚本即可。
    • 其他技巧:
      • 用 LaunchScreen.storyboard 替换启动图片。
      • 本地大图片都使用 webp
      • 资源按需加载,非必要资源都等到使用时再从服务端拉取。
  • 编译选项优化:
    • Optimization Level 在 release 状态设置为 Fastest/Smallest。
    • Strip Debug Symbols During Copy 在 release 状态设置为 YES。
    • Strip Linked Product 在 release 状态设为 YES。
    • Make String Read-Only 在 release 状态设为 YES。
    • Dead Code Stripping 在 release 状态设为 YES。
    • Deployment PostProcessing 在 release 状态设为 YES。
    • Symbols hidden by default 在 release 状态设为 YES。
  • 可执行文件优化:
    • 使用 LinkMap 分析库的使用情况
    • 三方库优化
      • 删除不使用的三方库。
      • 功能用的少但是体积大的三方库可以考虑自己重写。
      • 合并功能重复的三方库。
    • 代码分析
      • 用 AppCode 进行代码扫描
      • 去掉无用的类及文件
      • 清理 import
      • 去掉空方法
      • 去掉无用的 log
      • 去掉无用的变量
  • 其他技巧(选用):
    • 将业务打包成动态库。如果动态库的加载时机不控制好,会影响 App 的启动速度,权衡使用。
    • 动态化。将一部分 Native 界面用 RN/Weex 重写。
    • 去除 Swift 代码,Swift 的标准库是打包在安装包里的,一般都有 10M+。然后苹果官方说等到 Swift Runtime 稳定之后会合并到 iOS 系统里,那时候使用 Swift 就不会显著增加包大小了。
    • target -> Build Settings -> Other Link Flags 里添加如下指令,会把 TEXT 字段的部分内容转移到 RODATA 字段,避免苹果对 TEXT 字段的审核限制。当然其实跟安装包瘦身好像没有什么关系,所以除非快不行了否则不建议操作。
      -Wl,-rename_section,__TEXT,__cstring,__RODATA,__cstring -Wl,-rename_section,__TEXT,__gcc_except_tab,__RODATA,__gcc_except_tab -Wl,-rename_section,__TEXT,__const,__RODATA,__const -Wl,-rename_section,__TEXT,__objc_methname,__RODATA,__objc_methname -Wl,-rename_section,__TEXT,__objc_classname,__RODATA,__objc_classname -Wl,-rename_section,__TEXT,__objc_methtype,__RODATA,__objc_methtype
  • 苹果官方的策略:
    • 使用 xcasset 管理图片
    • 开启 BitCode

5、日常如何检查内存泄露?

  1. 静态分析:
    在 Xcode 菜单点击 Product 选择 Analyze (快捷键: Command + Shift + B)
    Xcode 会分析出可能 造成内存泄露的语句,
  2. 动态内存分析:
    2.1、分析内存泄露不能把所有的内存泄露查出来,有的内存泄露是在运行时,用户操作时才产生的。那就需要用到Instruments了。具体操作是通过 Xcode 打开项目,然后点击 Product --> Profile
    2.2、按上面操作,build 成功后跳出 Instruments 工具。选择 Leaks 选项,点击右下角的【choose】按钮,这时候项目程序也在模拟器或手机上运行起来了,在手机或模拟器上对程序进行操作。
    2.3、点击左上角的红色圆点,这时项目开始启动了,由于 Leaks 是动态监测,所
    以手动进行一系列操作,可检查项目中是否存在内存泄漏问题。
    橙色矩形框中所示绿色为正常,如果出现如右侧红色矩形框中显示红色,则表示出现内存泄漏。
    2.4、选中Leaks Checks,在 Details 所在栏中选择 CallTree,并且在右下角勾选 Invert Call TreeHide System Libraries,会发现显示若干行代码,双击即可跳转到出现内存泄漏的地方,修改即可。
  3. 分析内存泄露原因:
    3.1、检查 NSTimer 的使用:
    在需要释放的位置 释放 Timer, 即调用 timerinvalidate,并 timer 置为 nil;
    注意 NSTimer 的初始化方法(一些方法是 iOS 10 才适配),适配系统版本
    注意 循环引用问题, 合理使用 __weak__strong
    3.2、检查代理(Delegate)的使用:
    delegate 的强引用问题:使用 assign、weak 修改 delegate 属性
    3.3、检查 Block 使用:
    Block 最容易犯的就是循环引用问题。合理使用 __weak__strong

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

APP 启动分为热启动冷启动

  • 热启动是由于某种原因,APP的状态由running切换为suspend,但是此时APP并没有被系统kill掉,当我们再次把APP切换到前台的时候,APP会恢复之前的状态继续运行,这种就是热启动。我们平时所说的APP在后台的存活时间,其实就是APP能执行热启动的最大时间间隔。
  • 冷启动则是APP从被加载到内存到运行的状态。我们所说的启动优化一般是针对冷启动来说的。
  • 就苹果而言,它将启动分为两个阶段: pre-mainmain()。启动时间也是针对这两个阶段进行优化,下面我们也将从这两方面进行优化:
    • pre-main 阶段优化:
      如图所示:
    Total pre-main time: 866.86 milliseconds (100.0%)
         dylib loading time: 328.28 milliseconds (37.8%)
        rebase/binding time:  49.19 milliseconds (5.6%)
            ObjC setup time:  62.85 milliseconds (7.2%)
           initializer time: 426.38 milliseconds (49.1%)
           slowest intializers :
             libSystem.B.dylib :   7.52 milliseconds (0.8%)
    libMainThreadChecker.dylib :  37.19 milliseconds (4.2%)
          libglInterpose.dylib :  61.17 milliseconds (7.0%)
         libMTLInterpose.dylib :  22.23 milliseconds (2.5%)
                       MyMoney : 392.50 milliseconds (45.2%)
    
    pre-main 阶段主要由4部分组成:
    1. dylib loading(动态库的加载):
      这个阶段 dylib 会分析应用依赖的 dylib。由此可知: 应用依赖的 dylib 越少越好。在这一步优化的宗旨是减少 dylib 数量:
      1.1、移除不必要的 dylib ;
      1.2、合并多个 dylib 成一个 dylib 。
    2. rebase/binding :
      这个阶段主要是注册 Objc 类。所以指针数量越少越好。可做的优化有:
      2.1、清理项目中无用的类
      2.2、删减没有被调用到或者已经废弃的方法
      2.3、删减一些无用的静态变量
      可以通过 AppCode 等工具实现项目中未使用的代码扫描
    3. ObjeC setup :
      这个阶段基本不用优化。若 rebase/binding 阶段优化很好,本阶段耗时也会很少
    4. initializer :
      在这个阶段,dylib 开始运行程序的初始化函数,调用每个类和分类的 + load() 方法,调用 C/C++ 中的构造器函数。 initializer 阶段执行结束后, dylib 开始调用 main() 函数。在这一步,检查 + load() 方法,尽量把事情推迟到 + initialize() 方法里执行;并且控制 category 数量,去掉不必要的 category。
      在这里我们修改了部分原本代码中直接在 +load 函数初始化逻辑改为在 +initialize 中加载也就是到使用时才加载。
    • main() 函数之后的优化:
      • didFinishLaunchingWithOptions 优化
        • 目前 App 的 didFinishLaunchingWithOptions 方法里执行了多项项业务,有一大部分业务并不是一定要在这里执行的,如支付配置、客服配置、分享配置等。整理该方法里的业务,能延迟加载的就往后推迟,防止其影响启动时间。
        • 整理 didFinishLaunchingWithOptions ,将业务分级,对于非必须的业务移到首页显示后加载。同时,为了防止以后新加的业务继续往 didFinishLaunchingWithOptions 里扔,可以新建一个类负责启动事件,新加的业务可以往这边添加。
      • 首页渲染优化
        • 减少启动期间创建的 UIViewController 数量
          通过打符号断点-[UIViewController viewDidLoad] 发现,如果App 启动过程中创建了 12 个 UIViewController(包括闪屏),即在启动过程中创建了 12 个视图控制器,导致首页渲染时间较长
        • 延迟首页耗时操作
          如果 App 首页有个侧滑页面及侧滑手势,并且该页面是用 xib 构建的,将该 ViewController 改为代码构建,同时延迟该页面的创建时机,等首页显示后再创建该页面及侧滑手势,这个改动节省了 300-400ms。
        • 去除启动时没必要及不合理的操作
          项目中使用了自定义的侧滑返回,在每次 push 的时候都会截图,启动的时候自定义导航栏会截取两张多余首页的图片,并且截图用的 API (renderInContext) 性能较差,耗时 800ms 左右,去掉启动截图的操作。
          闪屏请求回调里写plist文件的操作放在主线程,导致启动时占用主线程,将文件读写移到子线程操作。

架构设计

1、设计模式是为了解决什么问题的?

编写软件过程中,程序员面临着来自耦合性内聚性以及可维护性可扩展性重用性灵活性等多方面的挑战,设计模式是为了让程序具有更好的:

  1. 代码重用性(相同功能代码,不用多次编写)
  2. 可读性(编程规范性)
  3. 可扩展性(增加新功能时十分方便)
  4. 可靠性(增加新功能后,对原来的功能没有影响)
  5. 实现高内聚,低耦合的特性

设计模式有 7 大原则:

  1. 单一职责原则
    一个类只负责一个职责,一个函数只解决一个问题
  2. 接口隔离原则
    大接口改多个小接口,原因外部不需要大接口这么多方法,更易控制
  3. 依赖反转原则
    即面向接口编程,尽量不要声明具体类,而是使用接口,实现解耦
  4. 里氏替换原则
    能出现父类的地方就一定可以用子类代替,即不要重写父类种的已实现的方法
  5. 开闭原则
    面向扩展开放,面向修改封闭。即不要修改一个已实现的类,更不要修改类中的方法,应该选择创建新类或者创建新方法的方式解决
  6. 迪米特法则
    又叫最少知道原则,即对外暴露的public方法尽量少,实现高内聚;且只和直接朋友通信
  7. 合成复用原则
    即不要重复自己,不要在项目内copy代码,应该选择将要copy的代码抽离出来,实现多个类复用

2、常见的设计模式有哪些?

  1. 单例模式
    意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
    主要解决:一个全局使用的类频繁地创建与销毁。

  2. 工厂模式
    简单工厂模式又叫静态工厂方法模式,就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。比如,一台咖啡机就可以理解为一个工厂模式,你只需要按下想喝的咖啡品类的按钮(摩卡或拿铁),它就会给你生产一杯相应的咖啡,你不需要管它内部的具体实现,只要告诉它你的需求即可。

  3. 抽象工厂模式
    抽象工厂模式是在简单工厂的基础上将未来可能需要修改的代码抽象出来,通过继承的方式让子类去做决定。
    比如:以上面的咖啡工厂为例,某天我的口味突然变了,不想喝咖啡了想喝啤酒,这个时候如果直接修改简单工厂里面的代码,这种做法不但不够优雅,也不符合软件设计的“开闭原则”,因为每次新增品类都要修改原来的代码。这个时候就可以使用抽象工厂类了,抽象工厂里只声明方法,具体的实现交给子类(子工厂)去实现,这个时候再有新增品类的需求,只需要新创建代码即可。

  4. 代理模式
    代理模式是给某一个对象提供一个代理,并由代理对象控制对原对象的引用。
    优点

    • 代理模式能够协调调用者和被调用者,在一定程度上降低了系统的耦合度
    • 可以灵活地隐藏被代理对象的部分功能和服务,也增加额外的功能和服务

    缺点

    • 由于使用了代理模式,因此程序的性能没有直接调用性能高
    • 使用代理模式提高了代码的复杂度

举一个生活中的例子:比如买飞机票,由于离飞机场太远,直接去飞机场买票不太现实,这个时候我们就可以上携程 App 上购买飞机票,这个时候携程 App 就相当于是飞机票的代理商。

  1. 观察者模式
    观察者模式是定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式又叫做发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。
    优点

    • 观察者模式可以实现表示层和数据逻辑层的分离,并定义了稳定的消息更新传递机制,抽象了更新接口,使得可以有各种各样不同的表示层作为具体观察者角色;
    • 观察者模式在观察目标和观察者之间建立一个抽象的耦合
    • 观察者模式支持广播通信;
    • 观察者模式符合开闭原则(对拓展开放,对修改关闭)的要求。

    缺点

    • 如果一个观察目标对象有很多直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间
    • 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃;
    • 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
  2. 策略模式
    策略模式是指定义一系列算法,将每个算法都封装起来,并且使他们之间可以相互替换。
    优点:遵循了开闭原则,扩展性良好。
    缺点:随着策略的增加,对外暴露越来越多。

3、谈谈单例的优缺点?

单例模式是一种常用的软件设计模式,在应用这个模式时,单例对象的类必须保证只有一个实例存在,整个系统只能使用一个对象实例。
优点
1. 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例
2. 避免对资源的多重占用
缺点
1. 没有接口,不能继承,与单一职责原则冲突
2. 一个类应该只关心内部逻辑,而不关心外面怎么样来实例化

4、聊聊 MVC、MVP、MVVM设计模式?

  • MVC:
    MVC即 Model-VIew-Controller。他是1970年代被引入到软件设计大众的。MVC模式致力于关注点的切分,这意味着 model 和 controller 的逻辑是不与用户界面(View)挂钩的。因此,维护和测试程序变得更加简单容易。
    MVC设计模式将应用程序分离为3个主要的方面:Model,View和Controller
    mvc模式

    • Model:Model代表了描述业务路逻辑,业务模型、数据操作、数据模型的一系列类的集合。这层也定义了数据修改和操作的业务规则。
    • View: View代表了UI组件,像CSS,JQuery,html等。他只负责展示从 controller 接收到的数据。也就是把model转化成UI。
    • Controller:Controller 负责处理流入的请求。它通过View来接受用户的输入,之后利用Model来处理用户的数据,最后把结果返回给View。Controll就是View和Model之间的一个协调者。
  • MVP
    MVP 模式把应用分成了 3 个主要方面: Model 、View 、 Presenter。
    MVP模式图解

    • Model:Model层代表了描述业务逻辑和数据的一系列类的集合。它也定义了数据修改和操作的业务规则。
    • View:View代表了UI组件,像CSS,JQuery,html等。他只负责展示从 Presenter 接收到的数据。也就是把模型(译者注:非 Model 层模型)转化成UI。
    • Presenter:Presenter 负责处理 View 背后所有的UI事件。它通过 View 接收用户输入,之后利用 Model 来处理用户的数据,最后把结果返回给 View 。与 View 和 Controller 不同, View 和 Presenter 之间是完全解耦的,他们通过接口来交互。另外 Presenter 不像 Controller 处理进入的请求。

    MVP模式关键点:

    1. 用户和 View 交互。
    2. View 和 Presenter 是一对一关系。意味着一个 Presenter 只映射一个 View 。
    3. View 持有 Presenter 的引用(译者注:应该是通过接口交互,并不直接引用Presenter),但是 View 不持有 Model 的引用(译者注:即使接口,也不会)。
    4. 在 View 和 Presenter 之间可以双向交互。
  • MVVM
    MVVM 即 Model-View-View Model。这个模式提供对 View 和 View Model 的双向数据绑定。这使得 View Model 的状态改变可以自动传递给View 。典型的情况是,View Model 通过使用 obsever 模式(观察者模式)来将 View Model 的变化通知给 Model。
    MVVM 模式图解

  • Model :Model 层代表了描述业务逻辑和数据的一系列类的集合。它也定义了数据修改和操作的业务规则。

  • View: View 代表了UI组件,像CSS,JQuery,html等。他只负责展示从 ViewModel 接收到的数据。也就是把模型转化成UI。

  • View Model :View Model 负责暴漏方法,命令,其他属性来操作 VIew 的状态,组装 model 作为 View 动作的结果,并且触发 view 自己的事件。

MVVM模式关键点:

  1. 用户和View交互。
  2. View 和 ViewModel 是多对一关系。意味着一个 ViewModel 可以映射多个 View。
  3. View 持有 ViewModel 的引用,但是 ViewModel 没有任何 View 的信息。
  4. View 和 ViewModel 之间有双向数据绑定关系。

5、常见的路由方案,以及优缺点对比

业内常见的路由方案有3种

  1. Url-scheme注册(MGJRouter)
    iOS系统中默认是支持 Url Scheme方式的,例如可以在浏览器中输入: weixin:// 就可以打开微信应用。自然在APP内部也可以通过这种方法来实现组件之间的路由设计。
    这种方式实现的原理是:在APP启动的时候,或者是向以下实例中的每个模块自己的 load 方法里面注册自己的断链(Url),以及对外提供服务(Block),通过url-scheme标记好,然后维护在url-router里面。 url-router中保存了各个组件对应的url-scheme,只要其它组件调用了 open url 的方法,url-router就会去根据url去查找对应的服务并执行。

    • URL 的命名规范
      遵循网上的 URIweb service 模式的资源通用表示方式)的格式。例如 appscheme://path : ctd://home/scan
    • 常见的案例
      • JLRouters
        本质可以理解为保存一个全局的mapkeyurlvalue是对应存放的block数组,urlblock都会常驻在内存中,当打开一个url时,JLRoutes就可以遍历这个全局的map,通过url来执行对应的block
      • MGJRouter
        蘑菇街的技术团队开源的一个router特点是使用简单方便JLRoutes的问题主要在于查找url的实现不够高效,通过遍历而不是匹配,还有就是功能偏多HHRouterurl查找是基于匹配,所以会更高效MGJRouter也是采用的这种方法,HHRouterViewController 绑定地过于紧密,一定程度上降低了灵活性。于是就有了 MGJRouter, 从数据结构上看它和 HHRouter 是一样的。
        蘑菇街方案不好的地方
        1. URL注册对于实施组件化是完全没有必要的,拓展性和可维护性都降低;
        2. 基于 Open-url 的方案的话,有一个致命缺陷:非常规对象无法参与本地组件间调度;但是可以通过传递parms来解决,但是这个区分了远程调用和本地调用的接口
        3. 模块内部是否仍然需要使用URL去完成调度?是没有必要的,为啥要复杂化?
        4. 当组件多起来的时候,需要提供一个关乎URL和服务的对应表,并且需要开发人员对这样一个表进行维护;
        5. 这种方式需要在APP启动时,每个组件需要到路由管理中心注册自己的URL及服务,因此内存中需要保存这样一份表,当组件多起来以后就会出现一些内存的问题;
        6. 混淆了本地调用远程调用,它们的处理逻辑是不同的正确的做法应该是把远程调用通过一个中间层转化成本地调用,如果把两者混为一谈,后期可能会出现无法区分业务的情况。比如对于组件无法响应的问题,远程调用可能直接显示一个404页面,但是本地调用可能需要做其它处理。如果不加以区分,那么就无法完成这种业务要求。 远程调用只能传递被序列化JSON的数据,像UIImage这样非常规的对象是不行的,所以如果组件接口要考虑远程调用,这里的参数与就不能是这类非常规对象。
      • routable-ios
      • HHRouter
    1. 优缺点:
      优点:

      • Url-Scheme 是借鉴前端Router系统App 内跳转方法 得出来的解决方案。所以不管是H5、RN、Android、iOS 都通用。
      • 服务器可以动态的控制页面的跳转,可以统一页面出问题后错误处理,三端统一。

      缺点:

      • URLmap规则是需要注册的,它们会在load方法里面写。写在load方法里面是会影响App启动速度的。
      • 大量的硬编码。URL链接里面关于组件页面的名字都是硬编码,参数也都是硬编码。而且每个URL参数字段都必须要一个文档进行维护,这个对于业务开发人员也是一个负担。而且URL短连接散落在整个App四处,维护起来实在有点麻烦。
      • 对于传递NSObject的参数,URL是不够友好的,它最多是传递一个字典。
  2. 利用Runtime实现的target-action方式(CTMediator)- 个人推荐
    相较于 url-scheme 的方式进行组件间的路由, runtime 的方式利用了 OC运行时 的特征,实现了组件间服务的自动发现,无需注册即可实现组建间的调用。因此,不管从维护性可读性扩展性来说,都是一个比较完美的方案。
    target-action 的原理:
    传统的中介者模式 。这个中间件 Mediator 会依赖其他组件,其他组件也会依赖 Mediator
    但是能不能让 Mediator 不在依赖组件,各个组件之间不再依赖,组件间调用只依赖中间件 Mediator 呢 ?
    官方 casa 大神的优化建议是这样的:
    利用 target-action 的方式,创建一个 target 的类,类中定义了一些 action 方法,这些方法的结果是返回一个 Controller 或其他 Object 。再给中间件 CTMediator 添加一个分类方法(category),定义组件外部可调用的方法接口,内部实现 perform: target: action 的方法。该方法主要通过 runtime 中的 NSClassFromString 获取 target 类和 NSSelectorFromString 获取方法名,这样就可以执行先去创建的 target 类中的方法得到返回值,在通过分类中的方法传值。
    优缺点:
    优点:

    • 充分的利用Runtime的特性,无需注册这一步Target-Action方案只有存在组件依赖Mediator这一层依赖关系。在Mediator中维护针对MediatorCategory,每个category对应一个TargetCategory中的方法对应Action场景。Target-Action方案也统一了所有组件间调用入口。
    • 有一定的安全保证,它对url中进行Native前缀进行验证

    缺点:

    • Target_ActionCategory中将常规参数打包成字典,在Target处再把字典拆包成常规参数,这就造成了一部分的硬编码
  3. protcol-class 注册
    通过协议绑定,核心思想和代理传值是一样的,遵循协议,实现协议中的方法。
    主要思路:

    • 创建一个头文件 CommonProtocol.h ,里面存放各个模块提供的协议。在各个模块依赖这个头文件,实现协议的方法。
    • 创建一个中间类 ProtocolMediator , 提供模块的注册和获取模块的功能(其实就是将类和协议名进行绑定,放在一个字典里,key是协议名字符串,value是类)。
    • 在各个模块中实现协议,核心代码如下:
    Class cls = [[ProtocolMediator sharedInstance] classForProtocol:@protocol(B_VC_Protocol)];   
    UIViewController<B_VC_Protocol> *B_VC = [[cls alloc] init];
    [B_VC action_B:@"param1" para2:222 para3:333 para4:444];
    [self presentViewController:B_VC animated:YES completion:nil];
    

    优缺点:
    优点:

    • 这个方案没有硬编码。

    缺点:

    • 每个Protocol都要向ModuleManager进行注册。
    • 组件方法的调用是分散在各处的,没有统一的入口,也就没法做组件不存在时或者出现错误时的统一处理。

6、如果保证项目的稳定性?

保证项目的稳定性从4个方面来说:

  1. 开发过程:
    • 开发规范
      • 代码规范
      • 自测习惯
      • XMind、PDMan、PostMan、Jenkins、Sonar 等工具使用
      • Git 、Svn 、禅道、TAPD等使用规范
    • FPS 监控 : CADisplayLink
    • CPU 使用率 : Instruments
    • 内存 : Instruments来查看leaks 、代码方面:Delegate、Block、 Block、 NSNotification
    • 启动时间: 优化
    • 耗电要求
  2. 代码检查:
    • CodeReview 习惯
    • 代码检查: OCLint、SwiftLint 、Sonar 等
  3. 测试:
    • 单元测试
    • UI 测试
    • 功能测试
    • 异常测试
  4. 线上:
    • 监控(日志系统):Crash监控、网络监控、性能监控、行为监控
    • 修复:JSPatch、RN

7、手动埋点、自动化埋点(无埋点)、可视化埋点

埋点:主要是为了收集数据和信息,用来跟踪应用使用的状况,后续用来进一步优化产品或是提供运营的数据支撑,包括访问数(Visits),访客数(Visitor),停留时长(Time On Site),页面浏览数(Page Views)和跳出率(Bounce Rate)等。
以大致分为两种:页面统计(track this virtual page view)、 统计操作行为(track this button by an event)。

  • 手动埋点(代码埋点)
    国内的主要第三方数据分析服务商,如百度统计、友盟、TalkingData、GrowingIO 等。
    优点:

    • 使用者控制精准,可以非常精确地选择什么时候发送数据
    • 使用者可以比较方便地设置自定义属性、自定义事件,传递比较丰富的数据到服务端

    缺点:

    • 埋点代价比较大,每一个控件的埋点都需要添加相应的代码,不仅工作量大,而且限定了必须是技术人员才能完成
    • 更新的代价比较大,每一次更新埋点方案,都必须改代码,然后通过各个应用市场进行分发,并且总会有相当多数量的用户不喜欢更新APP,这样埋点代码也就得不到更新了
    • 所有前端埋点方案都会面临的数据传输时效性和可靠性的问题了,这个问题就只能通过在后端收集数据来解决了
  • 自动化埋点(无埋点)

    • 无埋点是指开发人员集成采集 SDK 后,SDK 便直接开始捕捉和监测用户在应用里的所有行为,并全部上报,不需要开发人员添加额外代码;或者是说用户展现界面元素时,通过控件绑定触发事件,事件被触发的时候系统会有相应的接口让开发者处理这些行为。现在市面上主流无埋点做法有两种:一种是预先跟踪所有的渲染信息,一种是滞后跟踪的渲染信息。

    • 数据分析师/数据产品通过管理后台的圈选功能来选出自己关注的用户行为,并给出事件命名。之后就可以结合时间属性、用户属性、事件进行分析了。所以无埋点并不是真的不用埋点了。

    • 优点:

    1. 由于采集的是全量数据,所以产品迭代过程中是不需要关注埋点逻辑的,也不会出现漏埋、误埋等现象
    2. 无埋点方式因为收集的是全量数据,可以大大减少运营和产品的试错成本,试错的可能性高了,可以带来更多启发性的信息
    3. 无需埋点,方便快捷
    4. 减少了因为人员流动带来的沟通成本
    5. 无需开发,业务人员埋点即可
    6. 支持先上报数据,后进行埋点
    • 缺点:
    1. 缺点与可视化埋点相同,未解决个性化自定义获取数据的问题,缺乏数据获取的灵活性
    2. 企业针对SDK开发难度较大,一般由数据分析企业研发提供,使用第三方提供的埋点方案,有如下缺陷:
      a、数据源丢失,应用上报的数据上传至第三方服务端,可能造成
      企业泄密或用户的关键数据丢失;

      b、供应商数据丢包问题,无法根据应用特性进行改善
    3. 无埋点采集全量数据,给数据传输和服务器增加压力
    4. 仅仅支持客户端
  • 可视化埋点

    • 可视化埋点是指开发人员除集成采集 SDK 外,不需要额外去写埋点代码,而是由业务人员通过访问分析平台的 圈选 功能来出需要对用户行为进行捕捉的控件,并给出事件命名。圈选完毕后,这些配置会同步到各个用户的终端上,由采集 SDK 按照圈选的配置自动进行用户行为数据的采集和发送。
    • 优点:
    1. 可视化埋点很好地解决了代码埋点的埋点代价大和更新代价大两个问题。但是,可视化埋点能够覆盖的功能有限,目前并不是所有的控件操作都可以通过这种方案进行定制
    2. 埋点只需业务同学接入,无需开发支持
    • 缺点:
    1. 无法做到自定义获取数据,可视化埋点覆盖的功能有限
    2. 企业针对SDK开发难度相比代码埋点大
    3. 仅支持客户端行为

8、设计一个图片缓存框架(LRU)

9、如何设计一个 git diff

10、设计一个线程池?画出你的架构图

11、你的app架构是什么?有什么优缺点?为什么这么做?怎么改进?

  • MVC 架构。

  • 优点:

    1. 耦合性低
      视图层和业务层分离,这样就允许更改视图层代码而不用重新编译模型和控制器代码,同样,一个应用的业务流程或者业务规则的改变只需要改动MVC的模型层即可。因为模型与控制器和视图相分离,所以很容易改变应用程序的数据层和业务规则。
    2. 重用性高
      MVC模式允许使用各种不同样式的视图来访问同一个服务器端的代码,因为多个视图能共享一个模型,它包括任何WEB(HTTP)浏览器或者无线浏览器(wap),比如,用户可以通过电脑也可通过手机来订购某样产品,虽然订购的方式不一样,但处理订购产品的方式是一样的。由于模型返回的数据没有进行格式化,所以同样的构件能被不同的界面使用。
    3. 部署快,生命周期成本低
      MVC使开发和维护用户接口的技术含量降低。使用MVC模式使开发时间得到相当大的缩减,它使程序员(Java开发人员)集中精力于业务逻辑,界面程序员(HTML和JSP开发人员)集中精力于表现形式上。
    4. 可维护性高
      分离视图层和业务逻辑层也使得WEB应用更易于维护和修改。
  • 缺点:

    1. 完全理解MVC比较复杂
      由于MVC模式提出的时间不长,加上同学们的实践经验不足,所以完全理解并掌握MVC不是一个很容易的过程。
    2. 调试困难
      因为模型和视图要严格的分离,这样也给调试应用程序带来了一定的困难,每个构件在使用之前都需要经过彻底的测试。
    3. 不适合小型,中等规模的应用程序
      在一个中小型的应用程序中,强制性的使用MVC进行开发,往往会花费大量时间,并且不能体现MVC的优势,同时会使开发变得繁琐。
    4. 增加系统结构和实现的复杂性
      对于简单的界面,严格遵循MVC,使模型、视图与控制器分离,会增加结构的复杂性,并可能产生过多的更新操作,降低运行效率。
    5. 视图与控制器间的过于紧密的连接并且降低了视图对模型数据的访问
      视图与控制器是相互分离,但却是联系紧密的部件,视图没有控制器的存在,其应用是很有限的,反之亦然,这样就妨碍了他们的独立重用。

    依据模型操作接口的不同,视图可能需要多次调用才能获得足够的显示数据。对未变化数据的不必要的频繁访问,也将损害操作性能。

  • MVC 是苹果官方推荐的项目架构,相对于 MVPMVVM 架构来说入门相对的低一些;而且公司的项目不是很大,在综合人力成本等方面选择了 MVC 架构。

  • 针对 Controller 臃肿问题作出优化,将数据相关进行抽离管理,向 MVVM 模式靠拢。

12、看过哪些第三方框架的源码,它们是怎么设计的?

  • SDWebImage
    SDWebImage 组织架构:
    SDWebImage 组织架构
    SDWebImageDownloader :负责维持图片的下载队列;
    SDWebImageDownloaderOperation:负责真正的图片下载请求;
    SDImageCache:负责图片的缓存;
    SDWebImageManager:是总的管理类,维护了一个SDWebImageDownloader 实例和一个 SDImageCache 实例,是下载与缓存的桥梁;
    SDWebImageDecoder:负责图片的解压缩;
    SDWebImagePrefetcher:负责图片的预取;
    UIImageView+WebCache:和其他的扩展都是与用户直接打交道的。

    SDWebImage 图片加载流程
    SDWebImage 原理图

    1. 判断图片URL 是否为 nil,是则做出错处理并返回;
    2. URL MD5加密生成 key;
    3. 根据 key 读取内存(memory)缓存, 有则拿到图片返回,否则往下;
    4. 根据 key 读取磁盘(disk)缓存,有则拿到图片返回,否则往下;
    5. 根据URL 下载图片,下载成功则将图片保存到 内存和磁盘中返回图片
  • AFNetWorking
    AFNetWorking 组织架构:主要有5个模块

    1. AFHTTPSessionManager :是对 NSURLSession 的封装,负责发送网络请求,是 AFNetWotking 中使用最多一个模块
    2. AFNetworkingReachabilityManager :实时监测网络状态的工具类
    3. AFSecurityPolicy :网络安全策略的工具类,主要是针对于 Https 服务
    4. Serializstion :请求序列化工具类
      • AFURLRequestSerialization:请求入参序列化工具基类
      • AFURLResponseSerialization :请求回参序列化工具基类
        • AFJSONResponseSerializerJson 解析器,AFNetWorking 的默认解析器
        • AFXMLParserResponseSerializerXML 解析器
        • AFHTTPResponseSerializer : 万能解析器,直接返回二进制数据(NSData),服务器不会对数据进行处理
    5. UIKit : 对iOS UIKit 的扩展
    • AFNetworking 的可能面试考点
      1. AFNetworking 2.x怎么开启常驻子线程?为何需要常驻子线程?
        2.x 版本中 AFNetWorking 通过 RunLoop 开启了一个常驻子线程,具体代码是这样的:
        + (void)networkRequestThreadEntryPoint:(id)__unused object {
            @autoreleasepool {
                [[NSThread currentThread] setName:@"AFNetworking"];
        
                NSRunLoop *RunLoop = [NSRunLoop currentRunLoop];
                [RunLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
                [RunLoop run];
            }
        }
        
        + (NSThread *)networkRequestThread {
            static NSThread *_networkRequestThread = nil;
            static dispatch_once_t oncePredicate;
            dispatch_once(&oncePredicate, ^{
                _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
                [_networkRequestThread start];
            });
        
            return _networkRequestThread;
        }
        
        为何要开启常驻子线程?
        NSURLConnection 的接口是异步的,然后会在发起的线程回调。而一个子线程,在同步代码执行完成之后,一般情况下,线程就退出了。那么想要接收到 NSURLConnection 的回调,就必须让子线程至少存活到回调的时机。而AF让线程常驻的原因是,当发起多个http请求的时候,会统一在这个子线程进行回调的处理,所以干脆就让其一直存活下来
        上面说的一般情况,子线程执行完任务就会退出。子线程能够继续存活,就需要通过 RunLoop 来开启常驻线程。
      2. AFURLSessionManagerNSURLSession 的关系,每次都需要新建 manager 吗?
        AFNetWorkingmanagersession1对1的关系AFNetWorking 会在 manager 初始化的时候创建对应的 NSURLSession 。同样, AFNetWorking 也在注释中写明了可以提供一个配置好的 manager 单例来全局复用。
        这里复用 session 其实就是在利用 http2.0多路复用特点,减少访问同一个服务器时,重新建立 tcp 连接的耗时和资源。
      3. AFSecurityPolicy 如何避免中间人攻击?
        现在,由于苹果ATS的策略,基本都切到 HTTPS 了,HTTPS 的基本原理还是需要了解一下的,这里不做介绍。
        通常,首先我们要了解中间人攻击,大体就是黑客通过截获服务器返回的证书,并伪造成自己的证书,通常我们使用的 Charles/Fiddler 等工具实际上就可以看成中间人攻击。
        解决方案其实也很简单,就是 SSL PinningAFSecurityPolicyAFSSLPinningMode 就是相关设置项。
        SSL Pinning 的原理就是需要将服务器的公钥打包到客户端中, tls 验证时,会将服务器的证书和本地的证书做一个对比,一致的话才允许验证通过。
        typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
            AFSSLPinningModeNone,
            AFSSLPinningModePublicKey,    // 只验证证书中的公钥
            AFSSLPinningModeCertificate,    // 验证证书所有字段,包括有效期之内
        };
        
        由于数字证书存在有效期,内置到客户端后就存在失效后导致验证失败的问题,所以可以考虑设置为 AFSSLPinningModePublicKey 的模式,这样的话,只要保证证书续期后,证书中的公钥不变,就能够通过验证了。
      4. AFNetWorking 3.x 为什么不再需要常驻线程?
        AFNetWorking 2.x 使用 NSURLConnection ,痛点就是:发起请求后,这条线程并不能随风而去,而需要一直处于等待回调的状态。所以 AFNetWorking2.x 在权衡之后选择了常驻线程。
        AFNetWorking 3.x 之后使用了 NSURLSession
        self.operationQueue = [[NSOperationQueue alloc] init];
        self.operationQueue.maxConcurrentOperationCount = 1;
        self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
        
        AFNetWorking 3.x 使用 NSURLSession 解决了 NSURLConnection 的痛点,从上面的代码可以看出, NSURLSession 发起的请求,不再需要在当前线程进行代理方法的回调。可以指定回调的 delegateQueue ,这样我们就不用为了等待代理回调方法而苦苦保活线程了
        同时还要注意一下: 指定的用于接收回调的 QueuemaxConcurrentOperationCount 设为了 1 ,这里目的是想要让并发的请求串行的进行回调。
        为什么 3.0 中需要设置为 1 ?
        self.operationQueue.maxConcurrentOperationCount = 1;
        
        解答:功能不一样:3.0的operationQueue是用来接收NSURLSessionDelegate回调的,
        鉴于一些多线程数据访问的安全性考虑,
        设置了maxConcurrentOperationCount = 1 来达到串行回调的效果。
        而2.0的operationQueue是用来添加operation并进行并发请求的,所以不要设置为1。
        
        - (AFHTTPRequestOperation *)POST:(NSString *)URLString
                              parameters:(id)parameters
                                 success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
                                 failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
        {
            AFHTTPRequestOperation *operation = [self HTTPRequestOperationWithHTTPMethod:@"POST" URLString:URLString parameters:parameters success:success failure:failure];
            [self.operationQueue addOperation:operation];
            return operation;
        }
        
        为什么要串行回调?
        - (AFURLSessionManagerTaskDelegate *)delegateForTask:(NSURLSessionTask *)task {
            NSParameterAssert(task);
            AFURLSessionManagerTaskDelegate *delegate = nil;
            [self.lock lock];
            //给所要访问的资源加锁,防止造成数据混乱
            delegate = self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)];
            [self.lock unlock];
            return delegate;
        }
        
        这边对 self.mutableTaskDelegatesKeyedByTaskIdentifier访问进行了加锁目的保证多线程环境下的数据安全。既然加了锁,就算 maxConcurrentOperationCount 不设为 1,当某个请求正在回调时,下一个请求还是得等待一直到上个请求获取完所要的资源后解锁,所以这边并发回调也是没有意义的。相反多 task 回调导致的多线程并发,还会导致性能的浪费。所以 maxConcurrentOperationCount = 1

13、可以说几个重构的技巧么?你觉得重构适合什么时候来做?

  • 重构技巧:

    1. 重复代码的抽象提炼
    2. 冗长方法的分隔
    3. 嵌套条件分支的优化
    4. 去掉一次性的零时变量
    5. 消除过长参数列表
    6. 提取类或继承体系中的常量
    7. 让类提供应该提供的方法
    8. 拆分冗长的类
    9. 提取继承体系中重复的属性与方法到父类
  • 适合节点:

    1. 【增】在增加新功能的时候(增加新功能的时候,发现需要重构来便于新功能的添加)
    2. 【删】在扩展不再简单的时候(消除重复)
    3. 【改】修复缺陷(修复 Bug 的时候)
    4. 【查】代码审查(通过交流提出了很多修改的主意)

    重构是一个不断的过程。

14、开发中常用架构设计模式你怎么选型?

  • 首先我们从App 架构来说:
    针对项目的大小程度功能复杂程度模块的多少项目成本和时间等来选用 MVC 或者 MVVM 模式进行总的架构设计。
  • 其次项目中:
    1. 策略模式针对实现目标/功能的复杂度,判断情况选用 策略模式
    2. 观察者模式代理模式 针对实时情况而定。
    3. 工厂模式抽象工厂模式 :根据过程父子关系复杂程度子类种类数量多少程度,判断是否使用 工厂模式抽象工厂模式
    4. 适配器模式 : 高度自定义问题,前端/移动端 根据数据格式做适配。(比如说 电商SKU
      模式,列表 Cell 适配等)
    5. 单例模式 :根据模块在项目的 唯一性重要性 等作出判断。(比如:应用的配置信息,用户的个人信息,本地数据库进行操作,数据上传云端,通信管理类等)

15、你是如何组件化解耦的?

  • 首先得分层
    常见的结构有3层4层的。我一般用3层展现层业务层数据层
  • 根据功能
    • 基础功能组件:
      基础模块是任何一个App都需要用到的。如:性能统计NetworkingPatch网络诊断数据存储模块。对于基础模块来说,其本身应该是自洽的,即可以单独编译或者几个模块合在一起可以单独编译。所有的依赖关系都应该是业务模块指向基础模块的。
      基础模块之间尽量避免产生横向依赖
    • 业务组件:
      根据不同的业务拆分。如:支付业务组件、播放组件、商城组件、消息组件 等。
  • 组件方案采用 Runtime 实现的 target-action 方式(CTMediator

数据结构

1、数据结构的存储一般常用的有几种?各有什么特点?

  1. 顺序存储结构
    数据元素顺序存放,每个结点只有一个元素。存储位置反映数据元素间的逻辑关系
    • 存储密度大,但是插入、删除操作效率较差。(比如:数组:1-2-3-4-5-6-7-8-9-10,存储是按顺序的。再比如队列等)。
  2. 链式存储结构
    每个结点除了包含数据元素信息外还包含一组指针指针反映数据元素间的逻辑关系
    • 这种存储方式不要求存储空间连续,便于进行插入和删除操作,但是存储空间利用率较低
    • 另外,由于逻辑上相邻的数据元素在存储空间上不一定相邻,所以不能对其进行随机存取
  3. 哈希(散列)存储结构
    通过哈希函数解决冲突的方法,将关键字散列连续的 有限的地址空间内,并将哈希函数的值作为该数据元素的存储地址。
    • 其特点是存取速度快只能按关键字随机存取不能顺序窜出也不能折半存取
  4. 索引存储结构
    索引存储除了数据元素存储在一地址连续的内存空间外,尚需建立一个索引表。索引表中的索引指示结点的存储位置,兼有动态和静态的特性。

2、集合结构 线性结构 树形结构 图形结构

  1. 集合结构:就是一个集合,就是一个圆圈中有很多个元素,元素与元素之间没有任何关系 。
  2. 线性结构 :就是一个条线上站着很多个人。 这条线不一定是直的。也可以是弯的。也可以是直的,相当于一条线被分成了好几段的样子。 线性结构是一对一的关系
  3. 树形结构 :做开发的肯定或多或少的知道xml 解析 。树形结构跟他非常类似。也可以想象成一个金字塔。树形结构是一对多的关系
  4. 图形结构:这个就比较复杂了。 无穷、无边、 无向(没有方向)图形机构。你可以理解为多对多类似于我们人的交集关系

3、链表、单向链表、双向链表、循环链表

  • 链表:
    是一种物理存储单元非连续非顺序存储结构数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分

    1. 一个是存储数据元素的数据域。
    2. 另一个是存储下一个结点地址的指针域。

    相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。
    .

  • 单向链表:
    A->B->C->D->E->F->G->H。 这就是单向链表 ,H 是头 A 是尾,像一个只有一个头的火车一样。只能一个头拉着跑。

  • 双向链表:
    H<- A->B->C->D->E->F->G->H。 这就是双向链表有头没尾,两边都可以跑 ,跟地铁一样 到头了,可以倒着开回来。

  • 循环链表:
    A->B->C->D->E->F->G->H->A,绕成一个圈就像蛇吃自己的这就是循环。

4、数组和链表区别

  • 数组是可以在内存中连续存储多个元素的结构,在内存中的分配也是连续的,数组中的元素通过数组下标进行访问,数组下标从0开始
    优点:

    1. 按照索引查询元素速度快
    2. 按照索引遍历数组方便

    缺点:

    1. 数组的大小固定后就无法扩容了
    2. 数组只能存储一种类型的数据
    3. 添加,删除的操作慢,因为要移动其他的元素。

    适用场景:
    频繁查询,对存储空间要求不大,很少增加和删除的情况。

  • 链表:
    是一种物理存储单元非连续非顺序存储结构数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分

    1. 一个是存储数据元素的数据域。
    2. 另一个是存储下一个结点地址的指针域。

    相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。

5、堆、栈和队列

    • 堆是在程序运行时,而不是在程序编译时,申请某个大小的内存空间。即 动态分配内存,对其访问和对一般内存的访问没有区别。堆是指程序运行时申请的动态内存,而栈只是指一种使用堆的方法(即先进后出)。
    • 是一种比较特殊的数据结构,可以被看做一棵树的数组对象,具有以下的性质:
    1. 堆中某个节点的值总是不大于或不小于其父节点的值;
    2. 堆总是一棵完全二叉树。
    • 堆分为两种情况,有最大堆最小堆
      将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆,在一个摆放好元素的最小堆中,父结点中的元素一定比子结点的元素要小,但对于左右结点的大小则没有规定谁大谁小。
    • 堆常用来实现优先队列,堆的存取是随意的,这就如同我们在图书馆的书架上取书,虽然书的摆放是有顺序的,但是我们想取任意一本时不必像栈一样,先取出前面所有的书,书架这种机制不同于箱子,我们可以直接取出我们想要的书。
  • 栈:

    • 栈是一种具有先进后出的数据结构,又称为先进后出的线性表,简称 FILO(—First-In/Last-Out)结构。也就是说后存放的先取,先存放的后取,这就类似于我们要在取放在箱子底部的东西(放进去比较早的物体),我们首先要移开压在它上面的物体(放进去比较晚的物体)。
    • 栈是限定仅在表尾进行插入和删除操作的线性表。我们把允许插入和删除的一端称为栈顶,另一端称为栈底,不含任何数据元素的栈称为空栈。栈的特殊之处在于它限制了这个线性表的插入和删除位置,它始终只在栈顶进行。
    • 堆栈中定义了一些操作。两个最重要的是PUSHPOPPUSH操作在堆栈的顶部加入一个元素。POP操作相反,在堆栈顶部移去一个元素,并将堆栈的大小减一。
    • 系统会给栈自动分配内存空间
    • 常用:
    1. 递归(如:逆序输出)
    2. 语法检查,符号成对出现
    3. 数制转换
    4. 二叉树的一些操作
  • 队列:

    • 队列是一种先进先出(FIFO—first in first out)的数据结构,又称为先进先出的线性表,简称 FIFO(First In First Out)结构。也就是说先放的先取,后放的后取,就如同行李过安检的时候,先放进去的行李在另一端总是先出来,后放入的行李会在最后面出来。
    • 队列是只允许在一端进行插入操作、而在另一端进行删除操作的线性表。允许插入的一端称为队尾,允许删除的一端称为队头。它是一种特殊的线性表,特殊之处在于它只允许在表的前端进行删除操作,而在表的后端进行插入操作(队头做删除,队尾做插入)。和栈一样,队列是一种操作受限制的线性表。
      在这里插入图片描述

6、二叉树相关操作

7、输入一棵二叉树的根结点,求该树的深度?

  1. 如果一棵树只有一个结点,它的深度为1。
  2. 如果根结点只有左子树而没有右子树, 那么树的深度应该是其左子树的深度加1。
  3. 同样如果根结点只有右子树而没有左子树,那么树的深度应该是其右子树的深度加1。
  4. 如果既有右子树又有左子树, 那该树的深度就是其左、右子树深度的较大值再加1。
    public static int treeDepth(BinaryTreeNode root) {
        if (root == null) {
            return 0;
        }
        int left = treeDepth(root.left);
        int right = treeDepth(root.right);
        return left > right ? (left + 1) : (right + 1);
    }
    

8、输入一课二叉树的根结点,判断该树是不是平衡二叉树?

  • 重复遍历结点
  1. 先求出根结点的左右子树的深度;
  2. 然后判断它们的深度相差不超过1,如果否,则不是一棵二叉树;
  3. 如果是,再用同样的方法分别判断左子树和右子树是否为平衡二叉树,如果都是,则这就是一棵平衡二叉树。
  • 遍历一遍结点
    遍历结点的同时记录下该结点的深度,避免重复访问。
  • 方法:
bool IsBalanced_1(TreeNode* pRoot,int& depth){
    if(pRoot==NULL){
        depth=0;
        return true;
    }
    int left,right;
    int diff;
    if(IsBalanced_1(pRoot->left,left) && IsBalanced_1(pRoot->right,right)){
        diff=left-right;
        if(diff<=1 || diff>=-1){
            depth=left>right?left+1:right+1;
            return true;
        }
    }
    return false;
}
 
bool IsBalancedTree(TreeNode* pRoot){
    int depth=0;
    return IsBalanced_1(pRoot,depth);
} 

算法

1、时间复杂度

在计算机科学中,时间复杂性,又称时间复杂度
算法的时间复杂度是一个函数,它定性描述该算法的运行时间。这是一个代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述,不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的,亦即考察输入值大小趋近无穷时的情况。

2、空间复杂度

空间复杂度(Space Complexity)是对一个算法在运行过程中 临时占用存储空间大小的量度,记做S(n)=O(f(n))比如: 直接插入排序的时间复杂度是O(n^2)空间复杂度是O(1) 。而一般的递归算法就要有O(n)的空间复杂度了,因为每次递归都要存储返回信息。
一个算法的优劣主要从算法的执行时间和所需要占用的存储空间两个方面衡量:时间复杂度 & 空间复杂度

3、常用的排序算法

  1. 冒泡排序:

    • 原理:就是重复地走访过要排序的元素列,依次比较两个相邻的元素,顺序不对就交换,直至没有相邻元素需要交换,也就是排序完成。
    • 这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。
    • 冒泡排序是一种稳定排序算法。
    • 时间复杂度:最好情况(初始情况就是正序)下是o(n),平均情况是o(n²)
    /** 
     *	【冒泡排序】:相邻元素两两比较,比较完一趟,最值出现在末尾
     *	第1趟:依次比较相邻的两个数,不断交换(小数放前,大数放后)逐个推进,最值最后出现在第n个元素位置
     *	第2趟:依次比较相邻的两个数,不断交换(小数放前,大数放后)逐个推进,最值最后出现在第n-1个元素位置
     *	 ……   ……
     *	第n-1趟:依次比较相邻的两个数,不断交换(小数放前,大数放后)逐个推进,最值最后出现在第2个元素位置	
     */
    void bublleSort(int *arr, int length) {
        for(int i = 0; i < length - 1; i++) { //趟数
            for(int j = 0; j < length - i - 1; j++) { //比较次数
                if(arr[j] > arr[j+1]) {
                    int temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                }
            } 
        }
    }
    
  2. 选择排序

    • 选择排序(Selection sort)是一种简单直观的排序算法。原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素存放在序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到全部待排序的数据元素排完。
    • 选择排序是不稳定的排序方法
    • 时间复杂度:最好和平均情况下都是O(n²)
    /** 
     *	【选择排序】:最值出现在起始端
     *	
     *	第1趟:在n个数中找到最小(大)数与第一个数交换位置
     *	第2趟:在剩下n-1个数中找到最小(大)数与第二个数交换位置
     *	重复这样的操作...依次与第三个、第四个...数交换位置
     *	第n-1趟,最终可实现数据的升序(降序)排列。
     *
     */
    void selectSort(int *arr, int length) {
        for (int i = 0; i < length - 1; i++) { //趟数
            for (int j = i + 1; j < length; j++) { //比较次数
                if (arr[i] > arr[j]) {
                    int temp = arr[i];
                    arr[i] = arr[j];
                    arr[j] = temp;
                }
            }
        }
    }
    
  3. 直接插入排序

    • 插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序,
    • 插入排序的基本思想是:每步将一个待排序的记录,按其关键码值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止
    • 直接插入排序是稳定的排序算法。
    • 时间复杂度:最好情况(初始情况就是正序)下是o(n),平均情况是o(n²)
    /**
     *
     *	num[] 是已经排序好的,在插入一个数直接进行排序
     *
     */
    void insertSort2(int num[],int count)  {
        int i,j;
        for (i = 1; i < count; i++) {
            if (num[i] < num[i - 1]) { // 当前数比前一位的数小
                int temp = num[i]; // 记住 当前数
                for (j = i; j > 0; j--) { // 从当前数起 逆序
                    if (num[j - 1] > temp) num[j] = num[j - 1]; // 如果 当前数比前一位小,前一位后移
                    else break;
                }
                num[j] = temp; 
            }
        }
    }
    
  4. 二分插入排序

    • 由于在插入排序过程中,待插入数据左边的序列总是有序的,针对有序序列,就可以用二分法去插入数据了,也就是二分插入排序法。适用于数据量比较大的情况。
    • 二分插入排序的算法思想:
      算法的基本过程:
      (1)计算 0 ~ i-1 的中间点,用 i 索引处的元素与中间值进行比较,如果 i 索引处的元素大,说明要插入的这个元素应该在中间值和刚加入i索引之间,反之,就是在刚开始的位置到中间值的位置,这样很简单的完成了折半**;
      (2)在相应的半个范围里面找插入的位置时,不断的用(1)步骤缩小范围不停的折半,范围依次缩小为 1/2 1/4 1/8 …快速的确定出第 i 个元素要插在什么地方;
      (3)确定位置之后,将整个序列后移,并将元素插入到相应位置
    • 二分插入排序是稳定的排序算法。
    • 时间复杂度:最好情况(刚好插入位置为二分位置)下是O(log₂n),平均情况和最坏情况是o(n²)
    /**
     *	折半查找:优化查找时间(不用遍历全部数据)
     *
     *	折半查找的原理:
     *   1> 数组必须是有序的
     *   2> 必须已知min和max(知道范围)
     *   3> 动态计算mid的值,取出mid对应的值进行比较
     *   4> 如果mid对应的值大于要查找的值,那么max要变小为mid-1
     *   5> 如果mid对应的值小于要查找的值,那么min要变大为mid+1
     *
     */ 
     
    // 已知一个有序数组, 和一个key, 要求从数组中找到key对应的索引位置 
    int findKey(int *arr, int length, int key) {
        int min = 0, max = length - 1, mid;
        while (min <= max) {
            mid = (min + max) / 2; //计算中间值
            if (key > arr[mid]) {
                min = mid + 1;
            } else if (key < arr[mid]) {
                max = mid - 1;
            } else {
                return mid;
            }
        }
        return -1;
    }
    
  5. 希尔排序

    • 希尔排序(Shell’s Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。
    • 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,排序完成。
    • 希尔排序是不稳定排序算法。
    • 时间复杂度:O(n^(1.3—2))
    void shellSort(int num[],int count)
    {
        int shellNum = 2;
        int gap = round(count/shellNum);
    
        while (gap > 0) {
            for (int i = gap; i < count; i++) {
                int temp = num[i];
                int j = i;
                while (j >= gap && num[j - gap] > temp) {
                    num[j] = num[j - gap];
                    j = j - gap;
                }
                num[j] = temp;
            }
            gap = round(gap/shellNum);
        }
    }
    
  6. 快速排序

    • 快速排序(Quicksort)是对冒泡排序的一种改进
    • 它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
    • 快速排序是不稳定的排序算法
    • 时间复杂度:最差为O(n^2),平均为O(nlogn),最好为O(nlogn)
    void quickSort(int num[], int left, int right)
    {
        if (left >= right){ // 如果left >= right说明排序结束了
            return ;
        }
        // 变量key为基准数,在此规定基准数为序列的第一个数,即左指针指向的数
        int key = num[left];
        int i  = left;           //左指针
        int j  = right;          //右指针
        int temp;
        
        while (i != j) { // 该 while 循环结束一次表示比较了一轮
     		while(i < j && arr[j] >= key) { // 从右向左找第一个小于key的数
                 j--;
     		}
     		while(i < j && arr[i] < key) { // 从左向右找第一个大于等于key的数
                 i++;
     		}
     		
     		if(i < j) {
     			temp = arr[i];
     			arr[i] = arr[j];
             	arr[j] = temp;
            }
     	}
     	
     	arr[left] = arr[i];
     	arr[i] = key;
     	
        // 分治方法进行递归
        quickSort(num, left, i - 1);
        quickSort(num, i + 1, right);
    }
    
  7. 堆排序

    • 是指利用堆这种数据结构所设计的一种排序算法堆是一个近似完全二叉树的结构并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点
    • 在堆的数据结构中,堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。堆中定义以下几种操作:
      • 最大堆调整(Max Heapify): 将堆的末端子节点作调整,使得子节点永远小于父节点
      • 创建最大堆(Build Max Heap): 将堆中的所有数据重新排序
      • 堆排序(HeapSort): 移除位在第一个数据的根节点,并做最大堆调整的递归运算
    • 堆排序是一个非稳定的排序算法。
    • 时间复杂度:O(nlogn)
    void maxHeapify(int num[], int start, int end) {
        //建立父节点指标和子节点指标
        int dad = start;
        int son = dad * 2 + 1;
        while (son <= end) { //若子节点指标在范围内才做比较
            if (son + 1 <= end && num[son] < num[son + 1]) //先比较两个子节点大小,选择最大的
                son++;
            if (num[dad] > num[son]) //如果父节点大於子节点代表调整完毕,直接跳出函数
                return;
            else { //否则交换父子内容再继续子节点和孙节点比较
                EXCHANGE(num[dad], num[son])
                dad = son;
                son = dad * 2 + 1;
            }
        }
    }
    
    void heapSort(int num[], int count) {
        int i;
        //初始化,i从最後一个父节点开始调整
        for (i = count / 2 - 1; i >= 0; i--)
            maxHeapify(num, i, count - 1);
        //先将第一个元素和已排好元素前一位做交换,再重新调整,直到排序完毕
        for (i = count - 1; i > 0; i--) {
            EXCHANGE(num[0], num[i])
            maxHeapify(num, 0, i - 1);
        }
    }
    

4、字符串反转

- (NSString *)reversalString:(NSString *)originString{
    NSString *resultStr = @"";
    for (NSInteger i = originString.length -1; i >= 0; i--) {
      NSString *indexStr = [originString substringWithRange:NSMakeRange(i, 1)];
      resultStr = [resultStr stringByAppendingString:indexStr];
    }
  return resultStr;
}

5、链表反转(头差法)

  • 头插法 :
    将链表每个节点依次取下来头插到新链表,即为原链表的反转;因为改变了当前节点的 next 指向,必须先保存 next 地址。
    struct ListNode* reverseList(struct ListNode* head){
        //新链表的头指针
        struct ListNode* newhead = NULL;
        //需要头插的结点
        struct ListNode* cur = head;
        
        while(cur)
        {
            //保存需要头插结点的下一个节点
            struct ListNode* next = cur->next;
            //将cur头插到新链表
            cur->next = newhead;
            newhead = cur;
            cur = next;
        }
        return newhead;
    }
    
  • 迭代法
    遍历列表时,将当前节点的 next 指针改为指向前一个元素。由于节点没有引用其上一个节点,因此必须事先存储其前一个元素。在更改引用之前,还需要另一个指针来存储下一个节点。不要忘记在最后返回新的头引用。
    struct ListNode* reverseList(struct ListNode* head){
        struct ListNode* pre = NULL;
        //需要反转指向的结点
        struct ListNode* cur = head;
        
        while(cur)
        {
            //保存需要头插结点的下一个节点
            struct ListNode* next = cur->next;
            //将cur头插到新链表
            cur->next = pre;
            pre = cur;
            cur = next;
        }
        return pre;
    }
    

6、有序数组合并

- (void)merge {
    /*
     有序数组A:1、4、5、8、10...1000000,有序数组B:2、3、6、7、9...999998,A、B两个数组不相互重复,请合并成一个有序数组C,写出代码和时间复杂度。
     */
    //(1).
    NSMutableArray *A = [NSMutableArray arrayWithObjects:@4,@5,@8,@10,@15, nil];
    NSMutableArray *B = [NSMutableArray arrayWithObjects:@2,@6,@7,@9,@11,@12,@13, nil];
    NSMutableArray *C = [NSMutableArray array];
    int count = (int)A.count+(int)B.count;
    int index = 0;
    for (int i = 0; i < count; i++) {
        if (A[0]<B[0]) {
            [C addObject:A[0]];
            [A removeObject:A[0]];
        }
        else if (B[0]<A[0]) {
            [C addObject:B[0]];
            [B removeObject:B[0]];
        }
        if (A.count==0) {
            [C addObjectsFromArray:B];
            NSLog(@"C = %@",C);
            index = i+1;
            NSLog(@"index = %d",index);
            return;
        }
        else if (B.count==0) {
            [C addObjectsFromArray:A];
            NSLog(@"C = %@",C);
            index = i+1;
            NSLog(@"index = %d",index);
            return;
        }
    }
    //(2).
    //时间复杂度
    //T(n) = O(f(n)):用"T(n)"表示,"O"为数学符号,f(n)为同数量级,一般是算法中频度最大的语句频度。
    //时间复杂度:T(n) = O(index);
}

7、查找第一个只出现一次的字符(Hash查找)

两个思路:

  1. hash 不同编译器对字符数据的处理不一样,所以hash之前先把字符类型转成无符号类型;
  2. 空间换时间,用buffer数组记录当前只找到一次的字符,避免二次遍历。
# define SIZE 256
char GetChar(char str[])
{
  if(!str)
    return 0;
  char* p = NULL;
  unsigned count[SIZE] = {0};
  char buffer[SIZE];
  char* q = buffer;
  for(p=str; *p!=0; p++)
  {
    if(++count[(unsigned char)*p] == 1)
      *q++ = *p;
  }
  
  for (p=buffer; p<q; p++)
  {
    if(count[(unsigned char)*p] == 1)
    return *p;
  }
	return 0;
}

8、查找两个子视图的共同父视图

这个问的其实是数据结构中的二叉树,查找一个普通二叉树中两个节点最近的公共祖先问题。
假设两个视图为UIViewA、UIViewC,其中 UIViewA继承于UIViewB,UIViewB继承于UIViewD,UIViewC也继承于UIViewD;即 A->B->D,C->D

  • 方法1:
    - (void)viewDidLoad {
        [super viewDidLoad];
        Class commonClass1 = [self commonClass1:[ViewA class] andClass:[ViewC class]];
        NSLog(@"%@",commonClass1);
        // 输出:2018-03-22 17:36:01.868966+0800 两个UIView的最近公共父类[84288:2458900] ViewD
    }
    // 获取所有父类
    - (NSArray *)superClasses:(Class)class {
        if (class == nil) {
            return @[];
        }
        NSMutableArray *result = [NSMutableArray array];
        while (class != nil) {
            [result addObject:class];
            class = [class superclass];
        }
        return [result copy];
    }
     
    - (Class)commonClass1:(Class)classA andClass:(Class)classB {
        NSArray *arr1 = [self superClasses:classA];
        NSArray *arr2 = [self superClasses:classB];
        for (NSUInteger i = 0; i < arr1.count; ++i) {
            Class targetClass = arr1[i];
            for (NSUInteger j = 0; j < arr2.count; ++j) {
                if (targetClass == arr2[j]) {
                    return targetClass;
                }
            }
        }
        return nil;
    }
    
  • 方法2:
    方法一明显的是两层for循环,时间复杂度为 O(N^2) 一个改进的办法:我们将一个路径中的所有点先放进NSSet中.因为NSSet的内部实现是一个hash表,所以查询元素的时间的复杂度变成 O(1),我们一共有N个节点,所以总时间复杂度优化到了O(N)
    - (Class)commonClass2:(Class)classA andClass:(Class)classB{
        NSArray *arr1 = [self superClasses:classA];
        NSArray *arr2 = [self superClasses:classB];
        NSSet *set = [NSSet setWithArray:arr2];
        for (NSUInteger i =0; i<arr1.count; ++i) {
            Class targetClass = arr1[i];
            if ([set containsObject:targetClass]) {
                return targetClass;
            }
        }
        return nil;
    }
    

9、无序数组中的中位数(快排思想)

//求一个无序数组的中位数
int findMedian(int a[], int aLen)
{
    int low = 0;
    int high = aLen - 1;
    
    int mid = (aLen - 1) / 2;
    int div = PartSort(a, low, high);
    
    while (div != mid) {
    
        if (mid < div)  {
            //左半区间找
            div = PartSort(a, low, div - 1);
        }
        else  {
            //右半区间找
            div = PartSort(a, div + 1, high);
        }
    }
    //找到了
    return a[mid];
}

int PartSort(int a[], int start, int end)
{
    int low = start;
    int high = end;
    
    //选取关键字
    int key = a[end];
    
    while (low < high) {
    
        //左边找比key大的值
        while (low < high && a[low] <= key)  {
            ++low;
        }
        
        //右边找比key小的值
        while (low < high && a[high] >= key) {
            --high;
        }
        
        if (low < high) {
            //找到之后交换左右的值
            int temp = a[low];
            a[low] = a[high];
            a[high] = temp;
        }
    }
    
    int temp = a[high];
    a[high] = a[end];
    a[end] = temp;
    
    return low;
}

10、给定一个整数数组和一个目标值,找出数组中和为目标值的两个数

假设每个输入只对应一种答案,且同样的元素不能被重复利用。 示例:给定nums = [2, 7, 11, 15], target = 9 — 返回 [0, 1] 思路:

  • 第一层for循环从索引0到倒数第二个索引拿到每个数组元素,
  • 第二个for循环遍历上一层for循环拿到的元素的后面的所有元素。
class Solution {
    public int[] twoSum(int[] nums, int target) {
       int len = nums.length;
        int[] result = new int[2];
        for(int i = 0; i < len; i++){
            for(int j = i+1; j < len; j++){
                if(nums[i] + nums[j] == target){
                    result[0] = i;
                    result[1] = j; 
                    return result;
                }
            }
        }
        return result;
    }
}

网络

1、谈谈对 HTTP、HTTPS 的理解

  • HTTP协议超文本传输协议,他是基于TCP应用层协议

    • 无连接 无状态 的,需要通过cookies 或者 session 来保持会话
    • HTTP 分为部分:请求报文和响应报文
      • 请求报文个部分组成:请求行请求头空行请求体
      • 请求报文个部分组成:状态行响应头空行响应体
        HTTP 组成
    客户端请求:
    GET /hello.txt HTTP/1.1
    User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
    Host: www.example.com
    Accept-Language: en, mi
    服务端响应:
    
    HTTP/1.1 200 OK
    Date: Mon, 27 Jul 2009 12:28:53 GMT
    Server: Apache
    Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
    ETag: "34aa387-d-1568eb00"
    Accept-Ranges: bytes
    Content-Length: 51
    Vary: Accept-Encoding
    Content-Type: text/plain
    输出结果:
    
    Hello World! My payload includes a trailing CRLF.
    
    • URL 构成:

      preview

    • 协议构成:

      请求行、请求头、请求体

    • 常用的请求方式?

      答: GET、POST、PUT、DELETE、HEAD、OPTIONS?

    • GET 和 POST 的区别?

      • GET 把参数通过 & 拼接在URL 后面,POST 放在 body 里面
      • GET 有长度限制(一般2048字符),POST没有限制
      • 由于参数的存放,POST 相对于 GET 安全,相对安全是因为 POST 仍可以被抓包
      • GET 是可以被缓存的,POST 不可被缓存(后台使用Redis、Memcached 登记室除外)
  • HTTPS 协议:
    HTTPS是一种通过计算机网络进行安全通信的传输协议(以安全为目标),经由HTTP进行通信,利用SSL/TLS建立全信道,加密数据包。HTTPS使用的主要目的提供对网站服务器的身份认证,同时保护交换数据的隐私与完整性传输加密身份认证保证了传输过程的安全性)。
    PS: TLS是传输层加密协议,前身是SSL协议。HTTPS 的安全基础SSL
    通过抓包可以看到数据不是明文传输,而且HTTPS有如下特点:

    • HTTPS特点:
      基于HTTP协议,通过SSL或TLS提供加密处理数据、验证对方身份以及数据完整性保护
      这里写图片描述
      通过抓包可以看到数据不是明文传输,而且HTTPS有如下特点:
    1. 内容加密:采用混合加密技术,中间者无法直接查看明文内容
    2. 验证身份:通过证书认证客户端访问的是自己的服务器
    3. 保护数据完整性:防止传输的内容被中间人冒充或者篡改
    4. 混合加密:结合非对称加密和对称加密技术。客户端使用对称加密生成密钥对传输数据进行加密,然后使用非对称加密的公钥再对秘钥进行加密,所以网络上传输的数据是被秘钥加密的密文和用公钥加密后的秘密秘钥,因此即使被黑客截取,由于没有私钥,无法获取到加密明文的秘钥,便无法获取到明文数据。
    5. 数字摘要:通过单向hash函数对原文进行哈希,将需加密的明文“摘要”成一串固定长度(如128bit)的密文,不同的明文摘要成的密文其结果总是不相同,同样的明文其摘要必定一致,并且即使知道了摘要也不能反推出明文。
    6. 数字签名技术:数字签名建立在公钥加密体制基础上,是公钥加密技术的另一类应用。它把公钥加密技术和数字摘要结合起来,形成了实用的数字签名技术。
    7. 收方能够证实发送方的真实身份
    8. 发送方事后不能否认所发送过的报文
    9. 收方或非法者不能伪造、篡改报文
      在这里插入图片描述
      非对称加密过程需要用到公钥进行加密,那么公钥从何而来?其实公钥就被包含在数字证书中,数字证书通常来说是由受信任的数字证书颁发机构CA,在验证服务器身份后颁发,证书中包含了一个密钥对(公钥和私钥)和所有者识别信息。数字证书被放到服务端,具有服务器身份验证和数据传输加密功能。
    • HTTPS 的验证流程?
      归纳为5个步骤:

      1. 客户端发起一个http请求,告诉服务器自己支持哪些hash算法
      2. 服务端把自己的信息以数字证书的形式返回给客户端(证书内容有密钥公钥网站地址证书颁发机构失效日期等)。证书中有一个公钥来加密信息私钥由服务器持有
      3. 验证证书的合法性:
        客户端收到服务器的响应后会先验证证书的合法性(证书中包含的地址与正在访问的地址是否一致,证书是否过期)。
      4. 生成随机密码(RSA签名):
        如果验证通过,或用户接受了不受信任的证书,客户端就会生成一个随机的对称密钥(session key)并用公钥加密,让服务端用私钥解密,解密后就用这个对称密钥进行传输了,并且能够说明服务端确实是私钥的持有者。
      5. 生成对称加密算法:
        验证完服务端身份后,客户端生成一个对称加密的算法和对应密钥,以公钥加密之后发送给服务端。此时被黑客截获也没用,因为只有服务端的私钥才可以对其进行解密。之后客户端与服务端可以用这个对称加密算法来加密和解密通信内容了。
    • 数字证书都有哪些内容?

      1. Issuer – 证书的发布机构
        发布证书的机构,指明证书是哪个公司创建的(并不是指使用证书的公司)。出了问题具体的颁发机构是要负责的。
      2. Valid from,Valid to – 证书的有效期
        证书的使用期限。过了这个期限证书就会作废,不能使用。
      3. Public key – 公钥
        通常是一个字符串或数字进行加密/解密算法时使用。公钥和私钥都是密钥,只不过一般公钥是对外开放的,加密时使用;私钥是不公开的,解密时使用。
      4. Subject – 主题
        证书是颁发给谁了,一般是个人或公司名称机构名称公司网站的网址
      5. Signature algorithm- – 签名所使用的算法
        数字证书的数字签名所使用的加密算法,根据这个算法可以对指纹解密指纹加密的结果就是数字签名
      6. Thumbprint,Thumbprint algorithm – 指纹以及指纹算法(一种HASH算法
        指纹和指纹算法会使用证书机构的私钥加密后和证书放在一起
        主要用来保证证书的完整性,确保证书没有修改过。
        使用者在打开证书时根据指纹算法计算证书的hash值,和刚开始的值一样,则表示没有被修改过
    • 客户端如何检测数字证书是合法的并是所要请求的公司的?

      1. 首先应用程序读取证书中的 Issuer(发布机构),然后会在操作系统或浏览器内置的 受信任的发布机构中去找该机构的证书(为什么操作系统会有受信任机构的证书?先看完这个流程再来回答)。
      2. 如果找不到就说明证书是水货,证书有问题,程序给错误信息。
      3. 如果找到了,或用户确认使用该证书。就会拿上级证书公钥,解密本级证书,得到数字指纹。然后对本级证书的公钥进行数字摘要算法(证书中提供的指纹加密算法)计算结果,与解密得到的指纹对比。如果一样,说明证书没有被修改过。公钥可以放心使用,可以开始握手通信了。
    • 操作系统为什么会有证书发布机构的证书?

      1. 证书发布机构除了给别人发布证书外,自己也有自己的证书
      2. 在操作系统安装好时,受信任的证书发布机构的数字证书就已经被安装在操作系统中了,根据一些权威安全机构的评估,选取一些信誉很好并且通过一定安全认证的证书发布机构,把这些证书默认安装在操作系统中并设为信任的数字证书。
      3. 发布机构持有与自己数字证书对应的私钥,会用这个私钥加密所有他发布的证书及指纹整体作为数字签名。

2、TCP、UDP 和 Socket

  • TCP:(Transmission Control Protocol )传输控制协议,是一种面向连接的可靠的基于字节流传输层通信协议

    • 三次握手

      1. 客户端发送 SYN(SEQ=x)报文给服务器端,进入 SYN_SEND 状态。
      2. 服务器端收到 SYN 报文,回应一个 SYN (SEQ=y)ACK(ACK=x+1)报文,进入 SYN_RECV 状态。
      3. 客户端收到服务器端的 SYN 报文,回应一个 ACK(ACK=y+1) 报文,进入 Established 状态。
    • TCP 为什么要三次握手?而不是两次或者四次呢?

      1. 为了实现可靠数据传输, TCP 协议的通信双方, 都必须维护一个序列号, 以标识发送出去的数据包中, 哪些是已经被对方收到的。 三次握手的过程即是通信双方相互告知序列号起始值, 并确认对方已经收到了序列号起始值的必经步骤。
      2. 如果只是两次握手, 至多只有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认。
      3. 如果是四次或者其他,多于三次则是累赘,因为三次已经可以确保双方序列号都已被对方确认
    • 四次挥手:

      1. 某个应用进程首先调用 close,称该端执行“主动关闭”(active close)。该端的 TCP 于是发送一个 FIN 分节,表示数据发送完毕。
      2. 接收到这个 FIN 的对端执行 “被动关闭”(passive close),这个 FIN 由 TCP 确认
      3. 一段时间后,接收到这个文件结束符的应用进程将调用 close 关闭它的套接字。这导致它的 TCP 也发送一个 FIN
      4. 接收这个最终FIN的原发送端 TCP(即执行主动关闭的那一端)确认这个 FIN
        既然每个方向都需要一个 FIN 和一个 ACK,因此通常需要4个分节。
  • UDP:(User Datagram Protocol)用户数据报协议,是一种高速传输和实时性有较高的无连接的不可靠的 传输层协议

  • TCPUDP 的区别?

    1、连接性:TCP 面向连接,UDP 无连接

    2、可靠性:TCP 可靠的、保证消息顺序,UDP 不可靠(易丢包)、不能保证顺序

    3、模式:TCP 流模式,UDP 数据报格式

    4、资源损耗:TCP 更损耗数据

  • Socket:socket 是 “open—write/read—close” 模式的一种实现,那么socket 就提供了 这些操作对应的函数接口。使用socket 需要注意:

    • 心跳的保持
    • ping 和 pong 的呼应
    • 离开页面要断开,进入页面再重新连接

Object-C(简称 OC) 语言特性

1、多态

​ 多态表现为了三个方面动态类型动态绑定动态加载。之所以叫做多态,是因为必须到运行时(run time)才会做一些事情。

  1. 动态类型:

    编译器编译的时候是不能被识别的(如 id 类型),要等到运行时(run time),即程序运行的时候才会根据语境来识别。所以这里面就有两个概念要分清:编译时运行时

  2. 动态绑定 :

    动态绑定(dynamic binding)貌似比较难记忆,但事实上很简单,只需记住关键词@selector/SEL即可。

    而在OC中,其实是没有函数的概念的,我们叫消息机制所谓的函数调用就是给对象发送一条消息。这时,动态绑定的特性就来了。OC可以先跳过编译,到运行的时候才动态地添加函数调用,在运行时才决定要调用什么方法,需要传什么参数进去。这就是动态绑定,要实现他就必须用SEL变量绑定一个方法,最终形成的这个SEL变量就代表一个方法的引用。动态绑定的特定不仅方便,而且效率更高。

  3. 动态加载 :

    让程序在运行时添加代码模块以及其他资源。用户可以根据需要加载一些可执行代码和资源,而不是在启动时就加载所有组件。可执行代码中可以含有和程序运行时整合的新类。

2、继承

​ OC 不支持多继承,但是可以用 代理(Delegate) 来实现多继承。runtime 消息转发等实现伪多继承

4、代理(Delegate)

img

​代理是一种设计模式,以 @protocol 形式体现,一般是一对一传递

  • 代理为什么用 weak 修饰呢?block和代理的区别?
    • 一般以weak关键词以规避循环引用。
      weak 修饰指明该对象并不负责保持delegate这个对象,delegate 这个对象的销毁由外部控制。用 strong 修饰该对象强引用 delegate,外界不能销毁 delegate对象,会导致循环引用。
    • block 和代理的区别:
      1. 运行成本:代理运行成本低,block 运行成本高。
        因为block出栈需要将使用的数据从栈内存拷贝到堆内存,如果本身就在堆内存的话计数器会+1,使用完或block置为nil后才消除;
        delegate 只保留了一个指针对象,直接回调,没有额外的消耗。
      2. 写法更简练,更紧凑。
      3. block 注重结果的传输。
      4. block 要防止循环引用,善用 __weak__strong
      5. 公共接口,当方法较多后者调用太频繁建议永 delegate。

5、通知(NSNotificationCenter)

​ 使用观察者模式来实现的用于跨层传递信息的机制。传递方式是一对多的。

  • 如果实现通知机制?

    img

6、KVC (Key-value Coding)

键值编码是一种间接访问对象的属性使用字符串来标识属性,而不是通过调用存取方法,直接或通过实例变量访问的机制。非对象类型的变量将被自动封装或者解封成对象,很多情况下会简化程序代码。

  • KVC 底层实现原理:
    当一个对象调用setValue:forKey:方法时,方法内部会做以下操作:

    1. 判断有没有指定 keyset方法,如果有set方法,就会调用 set 方法,给该属性赋值
    2. 如果没有 set 方法,判断有没有跟 key 值相同且带有下划线的成员属性(_key) 如果有,直接给该成员属性进行赋值
    3. 如果没有成员属性 _key ,判断有没有跟key 相同名称的属性。如果有,直接给该属性进行赋值
    4. 如果都没有,就会调用 valueforUndefinedKeysetValue:forUndefinedKey: 方法
  • KVC 使用场景:

    • KVC 属性赋值
    • 添加私有成员变量
    • 字典和模型之间的互转

7、属性

OC 中,基本数据类型的默认关键字是atomic, readwrite, assign;普通属性的默认关键字是atomic, readwrite, strong。

  • 读写权限:readonly,readwrite(默认)

  • 原子性: atomic(默认),nonatomic。atomic读写线程安全,但效率低,而且不是绝对的安全,比如如果修饰的是数组,那么对数组的读写是安全的,但如果是操作数组进行添加移除其中对象的还,就不保证安全了。nonatomic禁止多线程,变量保护,提高性能

  • 引用计数:

    • retain/strong:表示指向并拥有该对象。其修饰的对象引用计数会增加1。该对象只要引用计数不为0则不会被销毁。当然强行将其设为nil可以销毁它。

    • assign:修饰基本数据类型,修饰对象类型时,不改变其引用计数,会产生悬垂指针,修饰的对象在被释放后,assign指针仍然指向原对象内存地址,如果使用assign指针继续访问原对象的话,就可能会导致内存泄漏或程序异常。这些数值主要存在于栈上。

    • weak:不改变被修饰对象的引用计数,所指对象在被释放后weak指针会自动置为nil,不会造成野指针。比如自定义 IBOutlet 控件属性也是用 weak (因为父控件的 subViews 数组已经对它有了一次强引用)。

    • copy:分为深拷贝和浅拷贝

      • 浅拷贝:对内存地址的复制,让目标对象指针和原对象指向同一片内存空间 会增加引用计数。
      • 深拷贝:对对象内容的复制开辟新的内存空间
        img
      • 可变对象的copy和mutableCopy都是深拷贝
        不可变对象的copy是浅拷贝,mutableCopy是深拷贝
        copy方法返回的都是不可变对象

8、@property 的本质是什么?ivar 、 setter 、getter 是如何生成并添加到这个类中的?

  • @property 的本质:
    @property = ivar + setter + getter
    即:@property 等于声明了ivar(数形变量),并实现了该属性的存取方法(setter + getter)。
  • @property 作为 OC 的一项特性,主要就在于封装对象中的数据。
    OC 通常把其所需要的各种数据保存为各种实例变量。实例变量一般通过“存取方法”(access method)来访问。其中,“获取方法” (getter)用于读取变量值,而“设置方法” (setter)用于写入变量值。

9、@synthesize 、@dynamic 的区别

  • @synthesize : 系统会自动生成该属性的 settergetter 方法。
  • @dynamic : 系统不会自动生成该属性的 settergetter 方法,需要用户自己去实现

10、UIView 和 CALayer 的关系?

  • UIView :
    • UIView 是 iOS 系统中的界面元素的基础,所有的界面元素都继承自它;
    • 它本身完全是由 CoreAnimation 来实现的;
    • 它真正绘图部分,是由 CALayer(CoreAnimation Layer) 类来管理的;
    • UIView 本身更像一个 CALayer 的管理器,访问它的根绘图和根坐标有关的属性(如:frame、bounds 等),实际上内部都是在访问他所包含的 CALayer 的相关属性;
    • UIView 的属性 layer ,对应的是他的 CALayer 实例。
    • UIView 可以响应时间,Layer 不可以,因为 UIView 继承自 UIResponder。
  • CALayer :
    • CALayer 类似于 UIView 的子 View 树形结构,也可以向它的 layer 上添加子 layer ,来完成某些特殊的表示;
    • UIView 的 layer 树形在系统内部分别是:
      1. 逻辑树,这里的代码是可操控的;
      2. 动画树,是一个中间层,系统就在这一层上更改属性,进行各种渲染操作;
      3. 显示树,其内容就是当前正在被显示在屏幕上的内容。
    • 动画的运作:对 UIView 的 subLayer(非主 Layer)属性进行更改,系统将会自动进行动画生成。
    • 坐标系统: CALayer 的坐标系统比 UIView 多了一个 anchorPoint 属性,使用 CGPoint 结构标识,值域是 0 ~ 1,是个比例值。
    • 渲染:当更新层,改变不能立即显示在屏幕上。当所有的层都准备好时,可以调用 setNeedsDisPlay 方法来重绘显示。
    • 变换:要在一个层中添加一个 3D 或仿射变换,可以分别设置层的 transformaffineTransform 属性。
    • 变形:Quartz Core 的渲染能力,使二维图像可以被自由操纵,就好像是三维的。图像可以在一个三维坐标系中以任意角度被旋转、缩放和倾斜。CATransform3D 的一套方法提供了一些魔术般的变换效果。

11、ViewController 不走 dealloc 的情况

  1. controller 使用了 NSTimer,并未对它进行销毁。
  2. block 块内使用了 self,造成了循环引用。
  3. 使用了 delegate,用了 strong 修饰,造成了强持有。记得用 weak/assign 修饰代理。
  4. controller 中使用了 WKWebView,- (void)addScriptMessageHandler:(id)scriptMessageHandlername:(NSString*)name 第一个参数使用self,造成了强持有。解决办法。

12、UICollectionView 自定义 layout 如何实现?

  1. 重写 prepareLayout 方法,并在里面事先就计算好必要的布局信息并存储起来。
  2. 基于 prepareLayout 方法中的布局信息,重写 collectionViewContentSize 方法返回 UICollectionView的内容尺寸。
  3. 重写 layoutAttributesForElementsInRect: 方法返回指定区域 cell、Supplementary View 和 Decoration View 的布局属性。
  4. 重写 layoutAttributesForItemAtIndexPath:;方法返回对应的 indexPath 的位置的 cell 的布局属性。
  5. 重写 layoutAttributesForSupplementaryViewOfKind: atIndexPath:,方法返回对应indexPath的位置的追加视图的布局属性,如果没有就不用重载。
  6. 重写 layoutAttributesForDecorationViewOfKind: atIndexPath:,方法返回对应indexPath的位置的装饰视图的布局属性,如果没有也不需要重载。
  7. 重写 shouldInvalidateLayoutForBoundsChange:,当边界发生变化时,是否应该刷新。

13、AppDelegate 的生命周期?从后台到前台调用了哪些方法?从前台到后台调用了哪些方法?第一次启动调用了哪些方法

  • 生命周期:
    1. 当应用程序启动时(不包括已在后台的情况下转到前台),调用此回调。 launchOptions 是启动参数,假如用户通过点击push通知启动的应用,这个参数里会存储一些push通知的信息。
      – (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions  {  
          NSLog(@"程序载入后");  
      }  
      
    2. 应用程序将要进入非活动状态执行(一般在程序运行时,有来电,锁屏,按HOME键,下拉通知栏,双击HOME键等情况会调用此方法),在此期间,应用程序不接受消息或事件 。在此方法中可以暂停正在进行的任务,如禁用定时器,暂停游戏等。
      - (void)applicationWillResignActive:(UIApplication *)application  {  
          NSLog(@"应用程序将要进入非活动状态(进入后台)");  
      }  
      
    3. 应用程序已经进入后台运行(应用程序支持后台运行),使用此方法来释放资源共享,保存用户数据,无效计时器,并储存足够的应用状态信息,等应用重新进入前台运行时将应用恢复到目前的状态。
      - (void)applicationDidEnterBackground:(UIApplication *)application  {  
          NSLog(@"应用程序已经进入后台运行");  
      }  
      
    4. 用程序将要进入活动状态执行,若应用不在后台状态,而是直接启动,则不会回调此方法。
      - (void)applicationWillEnterForeground:(UIApplication *)application  {  
          NSLog(@"应用程序将要进入前台运行");  
      }  
      
    5. 应用程序已经进入活动状态,即当应用程序重新启动,或者在后台转到前台,完全激活时,都会调用这个方法。
      - (void)applicationDidBecomeActive:(UIApplication *)application  {  
          NSLog(@"应用程序已进入前台,处于活动状态");  
      }  
      
    6. 当应用程序使用了太多的内存,操作系统会终止应用程序的运行,在终止前会调用这个方法。通常可以在这里进行内存清理工作,如释放一些当前不显示的页面,防止程序被终止。
      -(void)applicationDidReceiveMemoryWarning:(UIApplication *)application  {  
          NSLog(@"系统内存不足,需要进行清理工作");  
      }  
      
    7. 应用程序将要退出,且进程即将结束时会调用这个方法,一般很少主动调用,更多是内存不足时是被迫调用的,我们应该在这个方法里做一些数据存储操作和一些退出前的清理工作
      - (void)applicationWillTerminate:(UIApplication *)application  {  
          NSLog(@"应用程序将要退出");  
      }  
      
    8. 当系统时间发生改变时执行,应用中一些依赖系统时间的配置,需要在此方法中作相应改变。
      -(void)applicationSignificantTimeChange:(UIApplication *)application  {  
          NSLog(@"系统时间发生改变");  
      }  
      
  • 后台到前台:
    1. 应用程序将要进入活动状态,调用 applicationWillEnterForeground:
    2. 应用程序已经进入活动状态,调用 applicationDidBecomeActive
  • 前台到后台:
    1. 应用程序将要进入非活动状态,调用 applicationWillResignActive
    2. 应用程序已经进入后台运行,调用 applicationDidEnterBackground
  • 首次启动调用方法:
    1. 先调用 application: didFinishLaunchingWithOptions: 方法
    2. 调用 applicationDidBecomeActive ,应用程序已经进入活动状态。

14、NSCache 优于 NSDictionary 的几点?

NSCache 是一个非常奇怪的集合。默认为可变并且线程安全的。这使它很适合缓存那些创建起来代价高昂的对象。它自动对内存警告做出反应并基于可设置的成本清理自己。与NSDictionary相比,键是被retain而不是被拷贝的。

  1. 当系统资源将要耗尽时,NSCache可以自动删减缓存。如果采用普通的字典,那么就要自己编写挂钩,在系统通知时手动删减缓存,NSCache会先行删减时间最久为被使用的对象。
  2. NSCache 并不会拷贝键,而是会保留它。此行为用NSDictionary也可以实现,但是需要编写比较复杂的代码。NSCache对象不拷贝键的原因在于,很多时候键都是不支持拷贝操作的对象来充当的。因此NSCache对象不会自动拷贝键,所以在键不支持拷贝操作的情况下,该类比字典用起来更方便。
  3. NScache是线程安全的,NSDictionary不是。在开发者自己不编写加锁代码的前提下,多个线程可以同时访问NSCache。对缓存来说,线程安全通常是很重要的,因为开发者可能在某个线程中读取数据,此时如果发现缓存里找不着指定的键,那么就要下载该键对应的数据了。

15、idinstanceType 有什么区别?

  • 相同点:
    instancetypeid 都是万能指针,指向对象。
  • 不同点:
  1. id编译的时候不能判断对象的真实类型,instancetype 在编译的时候可以判断对象的真实类型。
  2. id可以用来定义变量,可以作为返回值类型,可以作为形参类型;instancetype 只能作为返回值类型。

16、self 和 super 的区别 ?

  • self 调用自己方法,super 调用父类方法
  • self 是类,super 是预编译指令
  • [self class] 和 [super class] 输出是一样的
  • self和super底层实现原理
  1. 当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;
  2. 而当使用 super 时,则从父类的方法列表中开始找,然后调用父类的这个方法;
  3. 当使用 self 调用时,会使用 objc_msgSend 函数:
    id objc_msgSend(id theReceiver, SEL theSelector, ...)
    
    第一个参数是消息接收者,第二个参数是调用的具体类方法的 selector,后面是 selector 方法的可变参数。以 [self setName:] 为例,编译器会替换成调用 objc_msgSend 的函数调用,其中 theReceiver 是 self,theSelector 是 @selector(setName:),这个 selector 是从当前 self 的 class 的方法列表开始找的 setName,当找到后把对应的 selector 传递过去。
  4. 当使用 super 调用时,会使用 objc_msgSendSuper 函数:
    id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
    
    第一个参数是个objc_super的结构体,第二个参数还是类似上面的类方法的selector
    struct objc_super {
    	id receiver;
    	Class superClass;
    };
    

17、setNeedsDisplaylayoutIfNeeded 两者是什么关系?

  • UIView 的 setNeedsDisplaysetNeedsLayout 两个方法都是异步执行的。
    • setNeedsDisplay 会自动调用 drawRect方法,这样可以拿到 UIGraphicsGetCurrentContext 进行绘制;
    • setNeedsLayout 会默认调用 layoutSubViews ,给当前的视图做了标记;layoutIfNeeded 查找是否有标记,如果有标记及立刻刷新。
      只有 setNeedsLayoutlayoutIfNeeded 这二者合起来使用,才会起到立刻刷新的效果。

Swift

1、Swift 和 OC

  • swift 和 OC 的联系
    1. swift 和 OC 共用一套运行时环境swift 和 OC 可以互相桥接,互相引用混合编程;
    2. OC 中很多类库,在 swift 中依然可以直接使用,只是语法上有些改变;
    3. OC 中的计数器ARC属性协议接口初始化扩展类命名参数匿名函数等绝大多数概念,在 swift 中继续有效。
    4. swift 中有 OC 没有的一些概念。比如:元组泛型 ,函数式编程模式(如 map、filter、reduce 等)等。
  • swift 相对于 OC 的优势
    1. swift 容易阅读,语法和文件结构简洁化。
    2. swift 更容易维护,文件分离后结构更清晰。
    3. swift 更加安全,它是类型安全的语言。
    4. swift 代码更少,语法更简洁,可以省去大量冗余的代码。
    5. swift 速度更快,运算性能更高。
语言Swift
优点1. 语法更简洁 2. 报错精准(报错的时候直接显示报错行)3. 定义变量简单(定义变量不用区分整型,浮点型等等,变量使用var,常量使用let。)4. 可视化互动效果(开发工具带来了Xcode Playgrounds功能,该功能提供强大的互动效果,能让Swift源代码在撰写过程中实时显示出其运行结果。) 5. 函数式编程的支持(Swift 语言本身提供了对函数式编程的支持;Objc 本身是不支持的,通过引入 ReactiveCocoa 这个库才可支持函数式编程。)
缺点1. Swift目前还没有得到全面性的推广 2. Swift 暂时还不稳定,在 Swift 5.0 之前 API 不稳定,之后变得稳定 3. 第三方库的支持不够多 4. App体积变大( App 体积大概增加 5-8 M 左右)5. 上线方式改变(在上线的时候,不能使用application Loader上传包文件,会提示你丢失了swift support files,应该使用xcode直接上传。)

2、Swift 的可选项类型(Optionals

swift 引用了可选项类型,用于处理变量值不存在的情况
Optionals 类似于 OC 中指向 nil 的指针,但是适用于所有的数据类型,而非仅仅局限于类,Optionals 相比于 OC 中的 nil 指针 ,更加安全和简明,并且也是 swift 诸多最强大功能的核心

3、Swift 中的 structclass

  • 相比于 OC 中的结构体,Swift 对结构体的使用比重大了很多,结构体成了实现面向对象的重要工具。
  • 相比于 C++ 和 OC 中的结构体只能定义一组相关的成员变量,在 Swift 不仅可以定义成员变量(属性),还可以定义成员方法。 因此在 Swift 中,我们可以把结构体看做是一种轻量级的类
  • Swift 中结构体不具有继承性,也不具备运行时类型强制转换、使用析构器和使用引用计等能力。
  • Swift 中 struct 是值类型,而 class 是引用类型。
    值类型的变量直接包含他们得数据,而引用类型的变量存储对他们的数据引用。
    因此引用类型的变量被称为对象,因此对一个变量操作可能影响另一个变量所引用的对象。
    而对于值类型都有他们自己的数据副本,因此对一个值类型的变量操作不可能影响到另一个值类型的变量。

4、swift 中 defer、guard?

  • defer
    defer 关键字提供了一个安全和简便的方式来处理这件事,当离开当前的代码块时,会执行defer对应的代码块。

    func openFileAction(){
        ///打开文件
        openFile()
        defer{
            closeFile()
        }
        ///读文件
        let isRead = readFile()
        guard isRead else {
            return
        }
    
        if emptyFile() {
            return
        }
        print("读取成功")
    }
    
  • guard
    guard 当条件满足的时候,会顺序执行,如果 guard条件不满足的时候,会进入 guard 内部,并执行 return 操作,终止代码的执行

5、Swift 中高阶函数有哪些?

  • map
    用于映射, 可以将一个列表转换为另一个列表。
    数组元素类型转换

    //swift为函数的参数自动提供简写形式,$0代表第一个参数,$1代表第二个参数
    let array = ["1", "2", "3"]
    let str1 = array.map({ "\($0)"}) //数组每个元素转成String类型
    //字符串数组转NSInteger类型数组
    let array1 = array.map { (obj) -> NSInteger in
        return NSInteger(obj) ?? 0
    }
    //NSInteger类型数组转字符串数组
    let array2 = array1.map { (obj) -> String in
        return String(obj)
    }
    print("array1: \(array1)")
    print("array2: \(array2)")
    //str1 ["1", "2", "3"]
    //array1: [1, 2, 3]
    //array2: ["1", "2", "3"]
    
  • flatMap
    功能跟map类似; 区别是flatMap会过滤nil元素, 并解包Optional
    flatMap 还可以将多维数组转换为一维数组,对于N维数组, map函数仍然返回N维数组。

    let array = [[1, 2, 3],[1, 2, 3],[1, 2, 3]]
    let arrret = array.flatMap{$0}
    let arrret1 = array.map{$0}
    print(arrret)
    print(arrret1)
    //[1, 2, 3, 1, 2, 3, 1, 2, 3]
    //[[1, 2, 3], [1, 2, 3], [1, 2, 3]]
    
  • filter
    用于过滤, 可以筛选出想要的元素

    let array = [1, 2, 3]
    let resultArray = array.filter { return $0 > 1 }
    print(resultArray)
    //[2, 3]
    
  • reduce
    reduce 方法把数组元素组合计算为一个值。

    //我们要求和
    let numbers = [2, -5, 9, 7, -2, 5, 3, 1, 0, -3, 8]
    //传统
    var result = 0
    for x in numbers {
        result += x
    }
    //使用reduce
    result = numbers.reduce(0,{$0+$1})
    

6、Swift 为什么将String,Array,Dictionary设计成值类型?

  • 值类型相比引用类型,最大的优势在于内存使用的高效。值类型在栈上操作,引用类型在堆上操作。栈上的操作仅仅是单个指针的上下移动,而堆上的操作则牵涉到合并、移位、重新链接等。也就是说Swift这样设计,大幅减少了堆上的内存分配和回收的次数。同时copy-on-write又将值传递和复制的开销降到了最低。
  • String,Array,Dictionary设计成值类型,也是为了线程安全考虑。通过Swift的let设置,使得这些数据达到了真正意义上的“不变”,它也从根本上解决了多线程内存访问和操作的问题。
  • 设计成值类型还可以提升API的灵活度。例如通过实现Collection这样的协议,我们可以遍历String,使得整个开发更加灵活高效。

7、Swift 中的 async/await? (swift 5.5 后,百度问)

  • async
    表示这个函数时可以异步执行的,也就是说执行这段代码是可以不阻塞当前线程。

    1. 函数/方法可以是异步的,属性也可以是异步的
    2. 当您将函数标记为异步时,您就允许它挂起。当一个函数挂起自己时,它也会挂起它的调用者。所以它的调用者也必须是异步的。
    3. 为了指出异步函数中它可能挂起一次或多次的位置,使用了 await 关键字。
    4. 当异步函数被挂起时,线程不会被阻塞。
    5. 当异步函数恢复时,从它调用的异步函数返回的结果流回原始函数,并从上次停止的地方继续执行。
  • await
    在函数、属性和初始值设定项中,await 可用于表达式可以解除当前线程阻塞;除此之外,await 还可以用于异步序列。

8、Swift 消息派发机制有几种?详细说说。

Swift 中派发机制分为直接派发函数表派发消息派发 三种。

  • 直接派发 (Direct Dispatch)
    直接派发是最快的, 不止是因为需要调用的指令集会更少, 并且编译器还能够有很大的优化空间。 例如函数内联等。 直接派发也有人称为静态调用。然而,对于编程来说直接调用也是最大的局限,而且因为缺乏动态性所以没办法支持继承。

  • 函数表派发 (Table Dispatch)
    函数表派发是编译型语言实现动态行为最常见的实现方式
    函数表使用了一个 数组来存储类声明的每一个函数的指针。大部分语言把这个称为 “virtual table”(虚函数表),Swift 里称为 “witness table”。每一个类都会维护一个函数表,里面记录着类所有的函数,如果父类函数被 override 的话,表里面只会保存被 override 之后的函数。一个子类新添加的函数,都会被插入到这个数组的最后。运行时会根据这一个表去决定实际要被调用的函数。

  • 消息派发 (Message Dispatch): Object-c的OO实现
    消息机制是调用函数最动态的方式。也是 Cocoa 的基石,这样的机制催生了 KVOUIAppearenceCoreData 等功能。这种运作方式的关键在于开发者可以在运行时改变函数的行为。不止可以通过 swizzling 来改变,甚至可以用 isa-swizzling 修改对象的继承关系, 可以在面向对象的基础上实现自定义派发。

swift 消息派发如图:做了总结

  • 派发的使用场景:
    • 值类型使用直接派发。
    • class协议的extension使用的是直接派发。
    • class协议的初始化声明使用的是函数表派发
    • class@obj extension 使用的是消息机制派发
  • 指定派发方式:
    • final

      1. final 允许类里面的函数使用直接派发。这个修饰符会让函数失去动态性。任何函数都可以使用这个修饰符,就算是 extension 里本来就是直接派发的函数。
      2. 这也会让 Objective-C 的运行时获取不到这个函数, 不会生成相应的 selector
    • dynamic
      dynamic 可以让类里面的函数使用消息机制派发

      1. 使用 dynamic, 必须导入 Foundation 框架,里面包括了 NSObject 和 Objective-C 的运行时。
      2. dynamic 可以让声明在 extension 里面的函数能够被 override
      3. dynamic 可以用在所有 NSObject 的子类和 Swift 的原声类。这就是为什么KVO的属性需要用dynamic修饰。
    • @objc
      函数能被 Objective-C运行时捕获到。
      使用 @objc 的典型例子就是给 selector 一个命名空间 @objc(abc_methodName),让这个函数可以被 Objective-C 的运行时调用。

    • @nonobjc
      禁止消息机制派发这个函数,不让这个函数注册到 Objective-C 的运行时里

    • @inline:直接派发。

其他

1、什么是静态库?什么是动态库?有什么区别?

库的本质可执行的二进制文件,是资源文件代码编译的一个集合。根据链接方式不同,可以分为动态库和静态库,其中系统提供的库都属于动态库

  • 静态库:
    静态库形式:.a.framework作用在进行链接生成可执行文件时,从静态库文件中拷贝需要的内容到最终的可执行文件中。
    被多次使用就有多份冗余拷贝。

    //在使用gcc编译时采用 -static选项来进行静态文件的链接:
    gcc -c main.c
    gcc -static -o main main.o
    
  • 动态库:
    静态库形式: .dylib.framework不在链接时将需要的二进制代码都拷贝到可执行文件中,而是拷贝一些重定位和符号表信息,当程序运行时需要的时候再通过符号表从动态库中获取(动态加载)。 系统只加载一次,多个程序共用,节省内存。

  • 动静态库区别:

    库名称优点缺点
    静态库1.目标程序没有外部依赖,直接就可以运行。2. 效率教动态库高。1. 会使用目标程序的体积增大。因为它将需要用到的代码从二进制文件中拷贝了一份
    动态库1. 不需要拷贝到目标程序中,不会影响目标程序的体积2. 同一份库可以被多个程序使用(因为这个原因,动态库也被称作共享库)。3. 编译时才载入的特性,也可以让我们随时对库进行替换,而不需要重新编译代码。实现动态更新1. 动态载入会带来一部分性能损失(可以忽略不计)2.动态库也会使得程序依赖于外部环境。如果环境缺少动态库或者库的版本不正确,就会导致程序无法运行(Linux lib not found 错误)。

2、直播篇幅

直播篇幅请点击跳转

  • 11
    点赞
  • 49
    收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:1024 设计师:我叫白小胖 返回首页
评论 2

打赏作者

一笑倾散风

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值