IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> iOS 面试题 -> 正文阅读

[移动开发]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:;在一个被观察属性发生改变之前,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 :分类

  • 给类添加新的方法

  • 不能给类添加成员变量

  • 通过@property定义的变量,只能生成对应的getter和setter的方法声明,但是不能实现getter和setter方法,同时也不能生成带下划线的成员属性

  • 是运行期决定的

    注意:为什么不能添加属性,原因就是category是运行期决定的,在运行期类的内存布局已经确定如果添加实例变量会破坏类的内存布局,会产生意想不到的错误。

extension :扩展

  • 可以给类添加成员变量,但是是私有的

  • 可以給类添加方法,但是是私有的

  • 添加的属性和方法是类的一部分,在编译期就决定的。在编译器和头文件的@interface和实现文件里

  • @implement一起形成了一个完整的类。

  • 伴随着类的产生而产生,也随着类的消失而消失

  • 必须有类的源码才可以给类添加extension,所以对于系统一些类,如NSString,就无法添加类扩展

    不能给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

// 第三阶段
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
- (void)forwardInvocation:(NSInvocation *)invocation</pre>

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 发送消息是安全的)

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对象对应一个线程,量级较轻(真正的多线程)

  2. NSOperation/NSOperationQueue 面向对象的线程技术

  3. GCD —— Grand Central Dispatch(派发) 是基于C语言的框架,可以充分利用多核,是苹果推荐使用的多线程技术

    对比:

线程类型对比备注
NSThread跨平台C语言标准库中的多线程框架过于底层使用很麻烦,需要封装使用.
NSOperation / NSOperationQueue更加面向对象 可以设置并发数量GCD 的封装
GCD(Grand Central Dispatch)iOS5后苹果推出的双核CPU优化的多线程框架,对A5以后的CPU有很多底层优化,C函数的形式调用 有点面向过程,不能直接设置并发数,需要写一些代码曲线方式实现并发推荐使用

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

10、NSOperationQueue 中的 maxConcurrentOperationCount 默认值

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

11、NSTimerCADisplayLinkdispatch_source_t 的优劣?

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

优化

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

  • 离屏渲染(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 Mode l。这个模式提供对 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,绕成一个圈就像蛇吃自己的这就是循环。

4、数组和链表区别

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

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

    缺点:

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

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

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

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

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

5、堆、栈和队列

  • 栈:一种常用的先进后出(FILO—First-In/Last-Out)的数据结构。常用:

    • 逆序输出
    • 语法检查,符号成对出现
    • 数制转换
    • 二叉树的一些操作 等
  • 队列:是一种先进先出(FIFO—first in first out)的数据结构。

  • :是一种比较特殊的数据结构,可以被看做一棵树的数组对象,具有以下的性质:

    1. 堆中某个节点的值总是不大于或不小于其父节点的值;
    2. 堆总是一棵完全二叉树。
    3. 将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。常见的堆有二叉堆、斐波那契堆等。

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、时间复杂度

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

2、空间复杂度

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

3、常用的排序算法

  1. 冒泡排序:

    原理:就是重复地走访过要排序的元素列,依次比较两个相邻的元素,顺序不对就交换,直至没有相邻元素需要交换,也就是排序完成。
    这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。

    冒泡排序是一种稳定排序算法。

    时间复杂度:最好情况(初始情况就是正序)下是o(n),平均情况是o(n2)

    void buddleSort(int num[],int count)
    {
        for (int i = 0; i < count - 1; i++) { 
            for (int j = 0; j < count - i - 1; j++) { // 注意 j < count - i - 1
                if (num[j] > num[j + 1]) EXCHANGE(num[j], num[j + 1]) // 如果 前面的数后面的大则交换
            }
        }
    }
    
  2. 选择排序

    选择排序(Selection sort)是一种简单直观的排序算法。原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素存放在序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到全部待排序的数据元素排完。

    选择排序是不稳定的排序方法。

    时间复杂度:最好和平均情况下都是O(n2)

    void selectSort(int num[],int count)
    {
        for (int i = 0; i < count; i++) {
            int min = i;
            for (int j = i; j < count; j++) { // 注意 j = i
                if (num[j] < num[min])  min = j;
            }
            if (i != min)   EXCHANGE(num[i], num[min]);//可以看出,最多交换count - 1次
        }
    }
    
  3. 直接插入排序

    插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序,
    插入排序的基本思想是:每步将一个待排序的记录,按其关键码值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止

    直接插入排序是稳定的排序算法。

    时间复杂度:最好情况(初始情况就是正序)下是o(n),平均情况是o(n2)

    /**
     *
     *	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(n2)

    void insertSortBinary(int num[],int count)
    {
        int i,j;
        for (i = 1; i < count; i++) {
            if (num[i] < num[i - 1]) {
                int temp = num[i];
                int left = 0,right = i - 1;
                while (left <= right) {
                    int mid = (left + right)/2;
                    if (num[mid] < temp) left = mid + 1;
                    else right = mid - 1;
                }
                //只是比较次数变少了,交换次数还是一样的
                for (j = i; j > left; j--) {
                    num[j] = num[j - 1];
                }
                num[left] = temp;
            }
        }
    }
    
  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 count,int left,int right)
    {
        if (left >= right){
            return ;
        }
        int key = num[left];
        int lp  = left;           //左指针
        int rp  = right;          //右指针
        while (lp < rp) {
            if (num[rp] < key) {
                int temp = num[rp];
                for (int i = rp - 1; i >= lp; i--) {
                    num[i + 1] = num[i];
                }
                num[lp] = temp;
                lp ++;
                rp ++;
            }
            rp --;
        }
        quickSort(num, count, left, lp - 1);
        quickSort(num, count, rp + 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,@17,@18, 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、无序数组中的中位数(快排思想)

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 来保持会话

    • 它包含了3个部分:

      • 请求报文:

        • 请求行:包含请求方法,URI,HTTP版本协议
        • 请求首部字段
        • 请求实体内容
      • 响应报文

        状态行:包含HTTP 版本,状态码,状态码原因短语

        响应首部字段

        响应内容实体

    • 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协议,由网景公司1995年发布,有时候两者不区分。
    通过抓包可以看到数据不是明文传输,而且HTTPS有如下特点:

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

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 状态。
    • 四次挥手:
      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关键词以规避循环引用。

5、通知(NSNotificationCenter)

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

  • 如果实现通知机制?

    img

6、KVO (Key-value Coding)

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

  • KVC 底层实现原理:

    当一个对象调用setValue:forKey: 方法时,方法内部会做以下操作:
     1.判断有没有指定key的set方法,如果有set方法,就会调用set方法,给该属性赋值
     2.如果没有set方法,判断有没有跟key值相同且带有下划线的成员属性(_key).如果有,直接给该成员属性进行赋值
     3.如果没有成员属性_key,判断有没有跟key相同名称的属性.如果有,直接给该属性进行赋值
     4.如果都没有,就会调用 valueforUndefinedKey 和setValue: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,不会造成野指针

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

      • 浅拷贝:对内存地址的复制,让目标对象指针和原对象指向同一片内存空间会增加引用计数
      • 深拷贝:对对象内容的复制开辟新的内存空间

      img

    • 可变对象的copy和mutableCopy都是深拷贝
      不可变对象的copy是浅拷贝,mutableCopy是深拷贝
      copy方法返回的都是不可变对象

其他

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、直播篇幅

  • 直播原理:
    直播原理:把主播录制的视频,推送到服务器,在由服务器分发给观众观看。
    直播环节推流端(采集、美颜处理、编码、推流)、服务端处理(转码、录制、截图、鉴黄)、播放器(拉流、解码、渲染)、互动系统(聊天室、礼物系统、赞)
    • 推流端:实现推流

      1. 音视频采集:
        通过摄像头、麦克风采集音视频。
        导入 AVFoundation.Framework ,从 captureSession 代理回调中获取音视频。
      2. 视频渲染:
        美颜、水印:使用 GPUImage 实现
      3. 音视频编码:
        1. 硬编码:视频使用 VideoToolBox 框架,音频使用 AudioToolBox 框架
        2. 软编码:视频压缩:视频编码 MPEGH.264x264 把视频元数据 YUV/RGB 编码 H.264
      4. 推流:
        1. 什么是推流?
          将采集的音视频数据通过流媒体协议发送到流媒体服务器
        2. muxing (封装)
          音视频封装成 FLVTS
        3. 推流技术
          a. 流媒体协议: RTMPRTSP , FLV , HLS
          b. 视频格式: FLVTS
          c. 音频格式: mp3ACC
        4. librtmp 把数据推送到流媒体服务器(基于 RTMP 协议)。
        5. HLS :直接使用 HTTP 协议上传。
    • 服务端:实现 CDN 分发

      1. 音视频转码
      2. 音视频录制
      3. 截图
      4. 鉴黄
      5. CDN 分发
    • 播放端:实现播放

      1. 拉流:(FLV 、 RMTP)
      2. 音视频解码:
      3. 渲染:
待补充
  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-09-05 11:07:57  更:2021-09-05 11:10:26 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年1日历 -2025/1/31 9:24:51-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码