介绍有哪些设计原则,并让比较详细的说了其中开闭原则在项目中的应用?

  • 单一职责原则
  • 开闭原则:OC中category,通过runtime 添加交换方法等,都是开闭原则
  • 里氏替换原则
  • 接口隔离原则
  • 依赖倒置原则
  • 迪米特法则
  • 组合/聚合复用原则

    介绍自己的过往的项目经验,会结合项目问一些架构向的思考

    架构是解决当项目增大,开发团队的人员越来越多,应用运营起来之后业务需求和功能需求日益增长。好的架构往往可以带来快速开发效率,和高效的代码管理。 我们可以看出我们的架构总是随着项目不断调整的,不是一尘不变的。 首先我们的架构设计应该牢记软件的设计原则。设计原则指导我们设计易理解的API设计和建立合理依赖关系。 常见的客户端架构有

  • MVC

    1. MVC最大的优势就是快速开发,当项目初期,追求快速上线的时候可以使用MVC,并且苹果提供了MVC的官方支持,项目初期无疑选择MVC是最佳的。
    2. MVC结构简单即使对于经验不那么丰富的开发者来讲维护起来也较为容易。
  • MVP
    1. 非常方便测试,因为view功能非常少,可以很方便测试UI细节
  • MVVM
    1. MVVM的优势就是,任务均摊每部分都承担各自的责任,结构清晰更加符合软件设计原则
    2. 其次就是可测试性强,我们只需要测试ViewModel就能够轻易的测试UI上的问题
    3. 通过观察者模式,来进行属性绑定,代码量将会小的多
  • VIPER
    1. 方便测试,指责明确,更容易对每个部分进行测试。
    2. 设计更清晰,兼容性更强
    3. 同时对团队人员要求更大,代码量比其他几种架构都大,并且管理要求更高。

对于组件化架构,其实脱胎于前端的设计思路,在前端SPA单页面应用,路由起到了很关键的作用。路由的作用主要是保证视图和 URL 的同步。在前端的眼里看来,视图是被看成是资源的一种表现。当用户在页面中进行操作时,应用会在若干个交互状态中切换,路由则可以记录下某些重要的状态,比如用户查看一个网站,用户是否登录、在访问网站的哪一个页面。而这些变化同样会被记录在浏览器的历史中,用户可以通过浏览器的前进、后退按钮切换状态。总的来说,用户可以通过手动输入或者与页面进行交互来改变 URL,然后通过同步或者异步的方式向服务端发送请求获取资源,成功后重新绘制 UI。 那么我们把这样的思路同样可以引入iOS,android。既可以保证每个页面独立性更强,又可以保证当我们需要快速修复bug的时候可以迅速替换为web页面。 常见的路由,有MGJRouter等。


iOS

iOS编译的过程和原理

编程语言分为两种,编译语言和直译式语言
编译语言就是 必须先通过编译器生成机器码,机器码可以直接在CPU上执行,执行效率较高,代表语言有 C++,OC,Swift等
直译式语言 是在执行的时候通过一个中间的解释器将代码解释为CPU可以执行的代码。较编译语言来说,直译式语言效率低一些,但是编写的更灵活。
iOS中编译都是依赖于Clang(swift) +LLVM 编译过程 预处理 -> 词法分析 -> 语法分析 -> 静态分析 -> 生成汇编指令 -> 汇编 -> 链接 -> 生成Mach-O文件 -> dyld动态链接 -> dSYM
具体iOS编译原理详情见这里

iOS 静态库和动态库

静态库和动态库是相对编译期和运行期的:静态库在程序编译时会被链接到目标代码中,程序运行时将不再需要改静态库;而动态库在程序编译时并不会被链接到目标代码中,只是在程序运行时才被载入,因为在程序运行期间还需要动态库的存在。

  1. 静态库:以.a.framework为文件后缀名。
  2. 动态库:以.tbd(之前叫.dylib) 和 .framework为文件后缀名。(系统直接提供给我们的framework都是动态库!)

    介绍常用属性修饰符,介绍 assign 和 weak 之间的区别。这块会延伸到内存管理相关,比如引用计数的方式。

    常用属性修饰符: weak , assign , strong , copy , nonatomic , atomic
    assign: 一般用来修饰基本的数据类型,包括基础数据类型和C数据类型。 assign声明的属性是不会增加引用计数的,声明的属性释放后,属性就不存在了。但是,指针没有置空过程,成为了野指针,如果新的对象被分配到了这个内存地址上,又会crash,所以一般只用来声明基本的数据类型。
    weak:弱引用,不增加引用计数。防止循环引用时使用,并且在所指向的对象被释放之后,weak指针会被设置的为nilweak引用通常是用于处理循环引用的问题,如代理及block的使用中,相对会较多的使用到weak
    当属性用weak修饰时,会将该指针存入一个weak表,该表是一个hash表,obj为键值,存储了一组obj_weak的地址。当属性使用weak修饰时,性能开销较大。
    strongstrong修饰的属性一般不会自动释放,在OC中,对象默认是强指针,当属性用strong 引用计数自动加1,该指针指向的对象不会由于其他指针指向的改变而销毁。
    copy: 在我们拷贝一个对象的时候,分为深拷贝和浅拷贝。
    深拷贝:对象拷贝 - 直接拷贝内容。源对象和副本对象指的是两个不同的对象,源对象引用计数器不变,副本对象引用计数器为1.
    浅拷贝:指针拷贝 - 将指针中的地址值拷贝一份。源对象和副本对象指的都是同一个对象,对象引用计数器+1,相当于retain.
    nonatomic:非原子操作,和atomic相对。atomic保证系统生成的 getter/setter 操作的完整性。注意这里只保证了getter/setter 操作,使用atomic并不是绝对线程安全的,当多个线程对该属性进行操作的时候,也会造成错误。

    介绍 runloop 相关的知识和在实际开发中的使用情况

    RunLoop顾名思义,是运行循环。它跟线程是一一对应的,每一个线程都有一个RunLoop,在需要的时候创建。RunLoop的作用很简单,就是保持线程不会退出,并且处理一些事件。 Runloops是线程的基础架构部分, CocoaCoreFundation 都提供了 run loop 对象方便配置和管理线程的 runloop 。每个线程,包括程序的主线程都有与之相应的 runloop 对象。

    1. int main(intargc,char* argv[])
    2. {
    3. @autoreleasepool
    4. {
    5. return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegateclass]));
    6. }
    7. }

    UIApplicationMain()函数,这个方法会为main thread设置一个NSRunLoop对象,这样就达到了可以在无人操作的时候休息,需要让它干活的时候又能立马响应。
    对其它线程来说,runloop默认是没有启动的,如果你需要更多的线程交互则可以手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要。
    在任何一个 Cocoa 程序的线程中,都可以通过以下代码来获取到当前线程的 runloop

    1. NSRunLoop *runloop = [NSRunLoop currentRunLoop];

    Runloop 通过model 来指定事件在运行循环中的优先级的。

    1. NSDefaultRunLoopModekCFRunLoopDefaultMode):默认,空闲状态
    2. UITrackingRunLoopModeScrollView滑动时
    3. UIInitializationRunLoopMode:启动时
    4. NSRunLoopCommonModeskCFRunLoopCommonModes):Mode集合

    实际应用中,可以利用Runloop完成一些耗时操作,比如tableview加载图片,还可以用来让定时器不随着屏幕滚动等操作停止等等。

    要求详细的介绍消息转发流程和事件响应链

    当你点击了屏幕会产生一个触摸事件,系统会将该事件加入到一个由UIApplication管理的事件队列中,UIApplication会从消息队列里取事件分发下去。 首先传给UIWindowUIWindow会使用hitTest:withEvent:方法找到此次触摸事件初始点所在的视图,找到这个视图之后他就会调用视图的touchesBegan:withEvent:方法来处理事件。 以消息的形式将事件发送给第一响应者,使其有机会首先处理事件。如果第一响应者没有进行处理,系统就将事件(通过消息)传递给响应者链中的下一个响应者,看看它是否可以进行处理。 在事件传递中,iOS优先响应动画,android优先响应事件

    控件的点击事件和添加在上边的手势谁先响应,并说明原因

  3. 当控件是普通控件如UIView时 先响应手势事件,不过手势是需要时间的,事件传递给了响应链的第一个响应对象。 响应链UIResponder的touchsBegan:withEvent方法,之后手势识别成功了,就会去cancel之前传递到的所有响应对象,于是就会调用它们的touchsCancelled:withEvent:方法。

  4. 当控件是UIButton, UISwitch, UISegmentedControl, UIStepper, UIPageControl时,在ios6以上,为了防止事件重叠,先响应控件事件,并且拦截了手势事件。 更具体的点击事件和手势冲突请参阅

    block 的实现原理

  1. struct Block_layout {
  2. void *isa;
  3. int flags;
  4. int reserved;
  5. void (*invoke)(void *, ...);
  6. struct Block_descriptor *descriptor;
  7. /* Imported variables. */
  8. };
  9. struct Block_descriptor {
  10. unsigned long int reserved;
  11. unsigned long int size;
  12. void (*copy)(void *dst, void *src);
  13. void (*dispose)(void *);
  14. };

首先面对这个面试题,要知道Block的类型分为三种: _NSConcreteStackBlock:在栈上创建的Block对象 _NSConcreteMallocBlock:在堆上创建的Block对象 _NSConcreteGlobalBlock:全局数据区的Block对象 而在ARC环境,大多数情况下编译器会适当地进行判断,会自动生成将Block从栈上复制到堆上的代码。 实现原理就是当一个__block 结构体被初始化的时候,原始值会成为该结构体的一个成员变量,同时其中的 __forwarding指针指向结构体自己。 与 Block 初始化相同的是,如果必要的话也会存在 copy_helperdispose_helper方法分别与 byref_keepbyref_destroy 这两个函数指针联系起来。 需要注意的是,常常会有面试官问如何捕获自动变量到block里面. 从以上源码我们可以知道,block其实就是匿名函数,并且禁止从栈区上修改自动变量,因为变量进入了block实际就是修改了变量的作用域,在几个作用域之间进行切换时,如果不加上这样的限制,变量的可维护性将大大降低。 所以当申明__block的时候,实际就是把变量的内存地址从栈中的放到了堆中。进而在block内部也可以修改外部变量的值。

谈 CoreAnimation 和 CoreGraphics 的区别

CoreGraphics是核心图形库,包含Quartz2D绘图API接口 CoreAnimation是核心动画库,用来做iOS,mac相关动画

聊对于 GCD 的理解,和 GCD 底层是如何进行线程调度的

首先,线程的生命周期包含:新建(create),就绪(ready),运行(run),阻塞(block), 死亡(dead) iOS的线程管理分为, pThreadNSThreadGCDNSOperation pthread :POSIX标准,基于C语言的通用跨平台线程管理API。 NSThread:最早期apple管理线程的api NSOperation:基于GCD的面向对象线程管理Api GCD:代替NSThread,C语言的线程管理Api
GCD是苹果提供的一套并行编程框架,帮助开发者改善应用的响应性能,它提供了一套易于使用的并发模型,可以在常见设计模式下提高优化代码潜在能力,比如单例模式就可以用GCD的方式创建,可以保证单例的线程安全。
GCD有一个线程池底层,并不需要开发者额外编写,线程池中的线程有着良好的重用性,当一段时间后这个线程没有被调用胡话,这个线程就会被销毁。那么我们只需要向线程队列中添加任务,线程队列调度就行了。 如果队列中存放的是同步任务,则任务出队后,底层线程池中会提供一条线程供这个任务执行,任务执行完毕后这条线程再回到线程池。 如果队列中存放的是异步的任务,当任务出队后,底层线程池会提供一个线程供任务执行,因为是异步执行,队列中的任务不需等待当前任务执行完毕就可以调度下一个任务,这时底层线程池中会再次提供一个线程供第二个任务执行,执行完毕后再回到底层线程池中。 这样就对线程完成一个复用,而不需要每一个任务执行都开启新的线程,也就从而节约的系统的开销,提高了效率。

GCD 中常见方法的使用 (group ,信号量,barrier 等

具体使用方法见阅

说@synchronized锁的实现原理,并说明其中可能存在的问题。同时介绍了 iOS 开发中常见的锁。

@synchronized是对mutex递归锁的封装,@synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作。最大的问题就是,效率低,传入对象必须等待之前的锁执行完成之后才能执行,无法达到异步的效果。
常见还有以下几种锁机制:

  • NSLockNSLock 只是在内部封装了一个 pthread_mutex,属性为 PTHREAD_MUTEX_ERRORCHECK
  • NSCondition:条件锁,通过条件变量pthread_cond_t 来实现的。条件变量有点像信号量,提供了线程阻塞与信号机制,因此可以用来阻塞某个线程,并等待某个数据就绪,随后唤醒线程,比如常见的生产者-消费者模式。
  • NSConditionLock: 借助 NSCondition 来实现,它的本质就是一个生产者-消费者模型。
  • NSRecursiveLock: 递归锁, 通过 pthread_mutex_lock 函数来实现,在函数内部会判断锁的类型
  • pthread_mutex_t: pthread下的互斥锁,互斥锁的实现原理与信号量非常相似,不是使用忙等,而是阻塞线程并睡眠,需要进行上下文切换。
  • dispatch_semaphore_t:信号量。
  • OSSpinLock: 自旋锁,定义一个全局变量,用来表示锁的可用情况即可。效率最高,但是容易发生锁的优先级反转,导致锁的不安全。

    介绍个人在开发过程中在哪些场景下用到过锁。

    使用锁的场景:
    如下载解压缩,排序显示,加载多张图片然后在都下载完成后合成一张整图等等。

    为什么不能在异步线程中更新页面,介绍原因

    在子线程中更新UI,更新的结果只是一个幻像:因为子线程代码执行完毕了,又自动进入到了主线程,执行了子线程中的UI更新的函数栈,这中间的时间非常的短,就让大家误以为分线程可以更新UI。如果子线程一直在运行,则子线程中的UI更新的函数栈主线程无法获知,即无法更新。

    详细的介绍了 KVC 和 KVO 的原理。

    kvc是利用了runtime动态机制,实现的一套通过使用属性名称来间接访问属性的方法

  • 程序优先调用setKey:属性值方法,代码通过setter方法完成设置。注意,这里的key是指成员变量名,首字母大小写要符合KVC的命名规范

  • 如果没有找到setName:方法,KVC机制会检查+(BOOL)accessInstanceVariablesDirectly方法有没有返回YES,默认返回的是YES,如果你重写了该方法让其返回NO,那么在这一步KVC会执行setValue: forUndefineKey:方法,不过一般不会这么做。所以KVC机制会搜索该类里面有没有名为_key的成员变量,无论该变量是在.h,还是在.m文件里定义,也不论用什么样的访问修饰符,只要存在_key命名的变量,KVC都可以对该成员变量赋值。
  • 如果该类既没有setKey:方法,也没有_key成员变量,KVC机制会搜索_isKey的成员变量。
  • 同样道理,如果该类没有setKey:方法,也没有_key_isKey成员变量,KVC还会继续搜索keyisKey的成员变量,再给他们赋值。
  • 如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的setValue:forUndefinedKey:方法,默认是抛出异常。

KVO 中文名称为键值观察,属于设计模式中的观察者模式,简单的说就是添加一个被观察对象A的属性,当被观察对象A的属性发生更改时,观察对象会获得通知,并作出相应的处理。KVO的底层是通过isa-swizzling实现的。
OC中,每个对象都有一个名为isa的指针,指向该对象的类。每个类中又描述了它的实例的特点,比如成员变量列表,成员函数列表。每一个对象都可以接收消息,而对象能够接收的消息列表都保存在它所对应的类中。NSObject就是一个包含isa指针的结构体。 每个实例对象都有一个isa的指针,指向该对象的类,而Class里也有个isa的指针, 指向meteClass。同样的,元类也是类,它也是对象。元类也有isa指针,最终指向的是一个根元类根元类的isa指针指向本身,这样形成了一个封闭的内循环。
而isa-swizzling,就是在运行时动态地修改 isa 指针的值,达到替换对象整个行为的目的。

在 webview 使用过程中存在的问题和解决方案

cookie问题UIWebView 的Cookie是通过 NSHTTPCookieStorage 统一管理,服务器返回时写入,发起请求时读取,Web 和 Native 通过该对象能共享 Cookie
而在 WKWebView 上,NSHTTPCookieStorage 不再是一个必经的流程节点,虽然 WKWebView 同样会对该对象写入Cookie,但并不是实时的,而且针对不同的系统版本延迟还不一样,另外发起请求时也不会实时去读取 Cookie。 这就导致每次 app 每次重启 Cookie 都会丢失,无法做到会话和Native同步。可以通过OAuth2协议解决。
功能性问题: wkwebview视图尺寸问题,默认跳转被屏蔽,需要手动交互。下载链接需要特殊处理,缓存问题

JSBridge 是如何实现的,以及和原生的调用关系。

JavaScript 是运行在一个单独的 JS Context 中(例如,WebView 的 Webkit 引擎、JSCore)。由于这些 Context 与原生运行环境的天然隔离,JSBridge 要实现

  1. 通信调用 Native 与 JS 通信
  2. 句柄解析调用。

    js和native通信

    jsbridge主要是通过注入 API 和 拦截 url scheme来实现和Native通信。

  3. api注入是通过 WebView 提供的接口,向 JavaScript 的 Context中注入对象或者方法,让 JavaScript 调用时,直接执行相应的 Native 代码逻辑,达到 JavaScript 调用 Native 的目的。

  4. 拦截scheme,不太优雅,放弃。
  5. Native 调用 JavaScript,其实就是执行拼接 JavaScript 字符串,从外部调用 JavaScript 中的方法,因此 JavaScript 的方法必须在全局的 window 上。

    谈对于 bitcode 的理解和作用。

    Bitcode是LLVM编译器的中间代码的一种编码,LLVM 将 C/C++/OC/Swift等编程语言 编译成 芯片平台上的汇编指令或者可执行机器指令数据。BitCode就是位于这两者直接的中间码。 因为中间码的存在,可以轻易创造新的语言,或者构造新的指令输出。Bitcode仅仅只是一个中间码不能在任何平台上运行,但是它可以转化为任何被支持的CPU架构,包括现在还没被发明的CPU架构,也就是说现在打开Bitcode功能提交一个App到应用商店,以后如果苹果新出了一款手机并CPU也是全新设计的,在苹果后台服务器一样可以从这个App的Bitcode开始编译转化为新CPU上的可执行程序,可供新手机用户下载运行这个App。

    APM (性能监控)

    详情参阅

    介绍自己比较熟悉的三方库的实现原理

    AFNetworking,SDWebImage YYkit, FFMpeg等

    AFNetworking 和 SDWebImage 相关的实际开发中的问题。


网络

比较详细的介绍 https 的过程。

  1. 客户端发起Https请求 -> 中间CDN,负载均衡节点转发等等 ->
  2. 连接到Server443端口 -> 服务器需要配置Https协议证书 ->
  3. 传输证书 -> 客户端解析证书 -> 传送加密信息 ->
  4. 服务端解析加密信息 -> 传输加密后的信息 -> 客户端还原解密信息。

如果现在做一个新的网络层框架,有哪些需要考量的点

  1. 既可以全局使用,又可以单独配置的Conifg。
  2. 缓存处理,缓存方式,缓存有效时长,缓存key。
  3. 重复网络请求处理。
  4. 网络请求释放中断处理。

    判断一个字符串是不是 ipv6 地址(要求尽全力的考虑所有异常的情况)

    a.这题是一题字符串处理的题目,题目要求我们判断一个字符串是不是合法的IPv4或IPv6字符串。
    我们可以把分别考虑这个字符串是不是IPv4字符串或IPv6字符串。
    b.判断一个字符串是不是IPv4字符串比IPv6稍难。
    我们从IPv4的特点入手:由4段数字组成,其中有三个’.’符号,每个数字的范围都在0~255之间,且每个数都是合法的数字(没有前导零)。
    所以我们可以先把这个字符串用‘.’分隔开,再统计一下’.’的个数,最后判断每个数字是否合法即可。
    需要注意的是IPv4字符串的每一个数字不允许存在前导0,但是单个的0是允许的。
    c.判断IPv6的思路和IPv4类似,拆分并且判断分隔符的个数,依次检查每个数的正确性。
    IPv6的地址不需要判断前导零,但是需要注意大小写都是合法的。

优化

过往开发中做过哪些优化向的工作,问的也比较详细。

卡顿产生原因是由于,在垂直同步机制内的一个刷新时间,CPU或者GPU的工作没有完成内容提交,会丢弃掉这一帧,屏幕会保持前一帧的显示,中间间隔时间过长肉眼就能看得出卡顿。
优化方案常见的有:

  1. cell的复用,
  2. cell高度计算比较费时,可以缓存高度。
  3. 减少视图层级,使用layer绘制元素。
  4. 对于图片尽量使用确定大小的图片,减少动态缩放,子线程预解码。多线程下载图片碎片,合并显示。
  5. 减少透明view
  6. 减少离屏幕渲染
  7. 合理使用光栅化
  8. 异步渲染(Facebook框架AsyncDisplayKit)

    对于内存泄漏的了解,以及介绍知道的解决方案。

    内存泄漏是指申请的内存空间使用完毕之后未回收。 一次内存泄露危害可以忽略,但若一直泄漏,无论有多少内存,迟早都会被占用光,最终导致程序crash。
    在iOS中我们可以使用,Instrument中leaks工具,检测内存泄漏的点,优化。
    主要可能造成泄漏原因有,block,delegate等的循环引用。

    如何检测项目中的卡顿问题(比如假死)

  • Core AnimationInstruments里的图形性能问题的测试工具。
  • view debugging,Xcode 自带的,视图层级。
  • reveal,视图层级。

    讲如何将一张内存极大的图片可以像地图一样的加载出来(只说实现思路)

    使用CATiledLayer加载大图,tile layer设置一个缩放区域的集合和重绘阈值,让scroll view在缩放时,绘制层根据这些区域和缩放阈值去重新绘制当前显示的区域

视频图像音频

对图像编解码的了解


算法

回文算法

判断一个整数是否是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。 判断一个字符串是不是对称的字符串,比如 abba 或者 aba 这样的就是对称的。

  1. 把正整数反转输出对比原数据,比如123321,通过取模取余反转原数据,如果相等就是回文数,最后用用一个或状态来返回两种可能的情况(仅限数字)
  2. 转化为字符串,通过回文字符串的移位比较,检查是否为回文。

    二叉树逐层打印

    二叉树翻转

    介绍 hash 算法的原理

    hash相当与把值映射到另外一个空间, 以键-值(key-indexed) 存储数据的结构,我们只要输入待查找的值即key,即可查找到其对应的值。通过hash函数将被查找的键转换为数组的索引,在理想的情况下,不同的键会被转换为不同的索引值,但是在有些情况下我们需要处理多个键被哈希到同一个索引值的情况,所以哈希查找的第二个步骤就是处理冲突也就是哈希碰撞。

    一个坦克从一个空间的起点到终点,中间在某些位置上有阻隔的情况下,判断从起点到终点是否有可行路径的算法题。

    找出一个页面中漏出部分面积最大的试图,重合的部分按照最上层的面积算漏出,会有时间复杂度的要求。

    面试资料:

    面试题持续整理更新中,如果你想一起进阶去大厂,不妨添加一下交流群1012951431
    面试题资料或者相关学习资料都在群文件中 进群即可下载!

iOS开发大厂面试题(Objective-C版本) - 图1