业界动态
iOS runloop 处理卡顿 ios runloop的使用场景
2024-12-23 13:11


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

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

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

  1. APP进程的mach port接收来自SpringBoard的触摸事件,主线程的RunLoop被唤醒,触发source1回调。
  2. source1回调又触发了一个source0回调,将接收到的IOHIDEvent对象封装成UIEvent对象,此时APP将正式开始对于触摸事件的响应
  3. source0回调将触摸事件添加到UIApplication的事件队列,当触摸事件出队UIApplication为触摸事件寻找最佳响应者。
  4. 寻找到最佳响应者之后,接下来的事情便是事件在响应链中传递和响应
    那么事件响应链是如何传递的呢 ? 可简称为 “由父及子” 的过程,即:
  • 触摸事件的传递是从父控件传递到子控件
  • 也就是从UIApplicaiton->window->寻找处理事件的最合适的view
    两个重要的方法:

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

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

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

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

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

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

  1. UIKit 并不是一个线程安全的类,UI操作涉及到渲染访问各种View对象的属性,如果异步操作下会存在读写问题,而为其加锁则会耗费大量资源并拖慢运行速度
  2. 另一方面因为整个程序的起点 UIApplication 是在主线程进行初始化所有的用户事件都是在主线程上进行传递(如点击、拖动),所以view只能在主线程上才能对事件进行响应而在渲染方面由于图像的渲染需要以60帧的刷新率在屏幕上同时更新,在非主线程异步化的情况下无法确定这个处理过程能够实现同步更新。

5、和的关系 ?

  1. 当调用 NSObject 的 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
  2. 当调用 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

6、如何使线程保活?

  1. 在执行的方法中添加while(true){},这样是模拟 的运行原理,结合 的信号量,在 中处理任务。
  2. 采用 的方式。参考这篇文章让子线程永远活着,这时就要用到常驻线程:给子线程开启一个 子线程执行完操作之后就会立即释放,即使我们使用强引用引用子线程使子线程不被释放,也不能给子线程再次添加操作,或者再次开启。
    子线程开启 的代码,先点击屏幕开启子线程并开启子线程 。

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

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

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

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

  • 系统默认注册了
  • 因为 为了响应 事件源, 会进行 切换以响应不同操作
    因为如果我们在主线程使用定时器,此时 的 为 ,即定时器属于 。那么此时我们滑动 时, 会切换到 ,因此在主线程的定时器就不在管用了,调用的方法也就不再执行了,当我们停止滑动时, 的 切换回 ,所以 就又管用了。
    为了防止此类情况发生,我们会将定时器加入 中,并设置

1、KVO 实现原理

  1. KVO是关于机制实现的
  2. 当某个类的对象属性第一次被观察时,系统就会在运行期动态地创建该类的一个派生类(NSKVONotifying_A),在这个派生类中重写基类中任何被观察属性的setter方法派生类在被重写的setter方法内实现真正的通知机制
  3. 如果原类为Person,那么生成的派生类名为NSKVONotifying_Person
  4. 每个类对象中都有一个isa指针指向当前类,当一个类对象的第一次被观察,那么系统就会偷偷将isa指针指向动态生成的派生类,从而在给被监控属性复制是执行的是派生类的setter方法
  5. 键值观察通知依赖于NSObject的两个方法:和在一个被观察属性发生改变之前,一定会被调用,这就会记录旧的值。而当改变发生后,会被调用,继而也会被调用。且重写观察属性的setter方法这种继承方式的注入在运行时而不是编译时实现的。

2、如何手动关闭 KVO ?

  1. 重写被观察对象的方法,返回
  2. 重写 ,返回NO。
    关闭 后,需要手动在赋值前后添加和,才可以收到观察通知。

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

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

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

  • 一个未注册的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. 我们观察的属性必须使用来定义。因此在编译器不会出现警告以及检查
  2. 对属性重构将导致我们的观察代码不再可用
  3. 复杂的语句要求对象正在观察多个值。这是因为所有的观察代码通过一个方法来指向
  4. 当释放观察者时不需要移除观察者

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

  • 对象:OC中的对象指向的是一个objc_object指针类型,从它的结构体中可以看出,它包括一个isa指针,指向的是这个对象的类对象,一个对象实例就是通过这个isa找到它自己的Class,而这个Class中存储的就是这个实例的方法列表属性列表成员变量列表等相关信息的。
  • 类:在OC中的类是用Class来表示的,实际上它指向的是一个objc_class的指针类型,对应的结构体如下:

从结构体中定义的变量可知,OC的Class类型包括如下数据(即:):(父类类对象);(类对象的名称);、(版本和相关信息);(实例内存大小);(实例变量列表);(方法列表);(缓存);(实现的协议列表);

当然也包括一个,这说明Class也是一个对象类型,所以我们,这里的isa指向的是元类对象(metaclass),元类中保存了创建类对象(Class)的类方法的全部信息

以下图中可以清楚的了解到OC对象、类、元类之间的关系

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

  • objc_ivar_list :
    存储了类的成员变量,可以通过或获取;另外这两个方法是用来获取类的属性列表的和,属性和成员变量是有区别的。
  • objc_method_list :
    存储了类的方法列表,可以通过获取。结构体如下:
  • objc_protocol_list :
    储存了类的协议列表,可以通过获取。结构体如下:

2、为什么要设计 ?

代表的是类对象的对象,存储了类的类方法,目的是将以及分开来,方便各司其职,符合单一职责设计原则

3、 & 区别?

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

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

4、 和 的区别?

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

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

OC对象中存储的其实被存储在而我们通过动态修改类的方法时,是修改在区域中存储的方法列表

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

  • 的加载是在运行时发生的,加载过程是:把category的添加到类对象上把category的添加到
  • 的方法执行顺序是根据类的决定的,即:xcode中的中的中的文件的顺序加载的。
  • 并不会替换掉同名的方法的,也就是说如果 category 和原来类都有 methodA,那么 category 附加完成之后,类的方法列表里会有两个 methodA并且添加的会排在原有类的的前面,因此如果存在的同名方法,那么在调用的时候,则会先找到 里的对应方法。

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

category :分类

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

extension :扩展

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

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

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

  1. 第一阶段:, 咨询接收者,询问它是否可以动态增加这个方法实现
  2. 第二阶段:在第一阶段中,接收者无法动态增加这个方法实现,那么将会进行,系统将询问是否有其他对象可能执行该方法,如果可以,系统将转发给这个对象处理。
  3. 第三阶段:在第二阶段中,如果没有其他对象可以处理,那么进行,系统将该消息相关的细节封装成对象,再给接收者最后一次机会,如果这里仍然无法处理,接收者将收到方法调用,此时程序将

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

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

  1. RunTime通过这个找到这个对象的类对象
  2. 从类对象中的中查找是否存在
  3. 若不存在,则会在 中查找
  4. 如果还是没找到,则会到中查找
  5. 仍然没找到的话,就会调用进行消息转发

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

  • IMP:是方法的实现,即:一段c函数
  • SEL:是方法名
  • Method:类型指针,它是一个结构体,如下:

使用场景:

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

调用进行方法交换

我们还可以给类动态添加方法,此时我们需要调用,该方法需要我们传递一个方法的,例如:

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

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

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

调用方式并不是采用方式调用的,而是直接采用函数的内存地址直接调用的多个类的load调用顺序,是依赖于中的文件顺序决定的,根据文件从上到下的顺序调用 ;子类和父类同时实现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,这里我给大家画个图梳理一下:

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

  • 调用方式是通过的方式调用的,此时所有的类都已经装载完毕
  • 子类和父类同时实现,父类的先被调用,然后调用子类的
  • 本类与同时实现会覆盖本类的方法,只调用
  • 一次(这也说明的调用方式采用的方式调用的)
  • 是主动调用的,只有当的时候才会触发

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

优点:

  • 动态化更新方案
    (例如: JSPatch):消息转发机制来进行JS和OC的交互,从而实现iOS的热更新
  • 实现多重代理
    利用消息转发机制可以无代码侵入的实现多重代理,让不同对象可以同时代理同个回调,然后在各自负责的区域进行相应的处理,降低了代码的耦合程度。
  • 间接实现多继承
    OC本身不支持多继承,但是可以通过消息转发机制在内部创建多个功能的对象,把不能实现的功能给转发到其他对象上去,这样就做出来一种多继承的假象。转发和继承相似,可用于为OC编程添加一些多继承的效果,一个对象把消息转发出去,就好像他把另一个对象中放法接过来或者“继承”一样。消息转发弥补了objc不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。
  • 预防线上奔溃
    利用消息转发机制对消息进行转发和替换,预防线上版本奔溃

缺点:

  • 消耗性能(延长了消息发送的周期,提高了成本)
  • bug 的定位更加困难

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

  • 关联对象 Associated Objects
  • 消息发送 Messaging
  • 消息转发 Message Forwarding
  • 方法调配 Method Swizzling
  • “类对象” NSProxy Foundation | Apple Developer documentation
  • KVC、KVO about Key-Value Coding
  • 动态获取 class 和 slector

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

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

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

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

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

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

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

  • 和函数类似, 只不过是直接定义在另一个函数里的, 和定义它的那个函数共享同一个范围内的东西。可以实现闭包, 有些人也称它作
  • 结构如下:
  • 由上图可知,实际上是由6部分组成的:
  • 指针
  • ,用于按位表示的一些附加信息
  • ,保留变量
  • ,函数指针,指向具体的实现的函数调用地址
  • ,从它的结构体可以看出,主要表示该的附加描述信息,主要是大小,以及函数的指针
  • ,捕获的变量,能访问它的外部的局部变量,就是因为将这些变量(或变量地址)复制到了结构体中

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

是类。 它有三种类型:分别是ARC下:__和,切换到非ARC下的;

  1. __ :全局静态block,不访问任何外部变量,isa 指向
    1.1. 这种块,运行时也无须有状态来参与。
    1.2. 全局块声明在, 在
  2. __ :保存在堆上的block,引用计数为0时销毁,isa指向
    一个类型做调用,那会将这个block从栈复制到堆上,堆上的这个类型就是,所以类型的是存储在堆区。如果对一个类型block做操作,那这个block的引用计数+1。
    在环境下,编译器会根据情况,自动将栈上的block复制到堆上。
  3. :保存在栈上的block,函数返回时销毁,isa指向
    如果一个里面访问了,那它就是一个,它在内存中存储在栈区,栈区的特点就是其释放不受开发者控制,都是由系统管理释放操作的,所以在调用类型时要注意,一定要确保它还没被释放。如果对一个类型block做操作,那会将这个block从栈复制到堆上

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

  • 没有被修饰的,体中对这个变量的引用是,在中是不能被修改的
    通过修饰的,体中对这个变量的引用是它会生成一个,复制这个变量的,从而达到的作用。
  • 的变量截获:
  • 会将体内引用外部变量的变量进行拷贝,将其拷贝到的数据结构中,从而可以在体内访问或修改外部变量。
  • 外部变量未被修饰时,数据结构中捕获的是外部变量的值,通过修饰时,则捕获的是对外部变量的指针引用

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

4、在修改,需不需要添加 ?

  • 如果修改的是的存储内容的话,是不需要添加修饰的。
  • 如果修改的是 对象的本身,那必须添加修饰。 参考的变量捕获(第3点)

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

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

也就是说,下只存在

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

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

6、可以用修饰吗?

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

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

  • 修饰的变量,不会出现引用计数+1,也就不会造成强持有外部变量,这样也就不会出现循环引用的问题了。
  • 但是,我们的内部执行的代码中,有可能是一个异步操作,或者延迟操作。此时引用的外部变量可能会变成,导致意想不到的问题,而我们在内部通过修饰这个变量时,会在执行过程中强持有这个变量,此时这个变量也就不会出现的情况,当执行完成后,这个变量也就会随之释放了。
  • 那么问题来了: 需要用 修饰吗?如果不用,那为什么呢?
    内部并没有使用 , 在 或 中 View 并没有持有 ,所以这个 只是一个 。当执行完 就出栈释放掉了,所以不会造成循环引用。

8、 发生 的时机?

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

  • 作为方法或函数的返回值时,编译器会自动完成copy操作。
  • 当赋值给通过或修饰的类型的成员变量时。
  • 当 作为参数被传入方法名带有 的 方法或 的 时。

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

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

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

因此

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

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

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

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

  • 进程:
  • 进程是一个具有独立功能的程序关于某次数据集合的一次运行活动,他是操作系统分配资源的基本单位
  • 进程是指系统正在运行中的一个应用程序,就是一段程序执行的过程。我们可以理解为手机上的一个app。
  • 每个进程之间是独立的。每个进程均运行在起专用且受保护的内存空间内,拥有独立运行所需的全部资源。
  • 进程是操作系统进行资源分配的单位
  • 线程:
  • 程序执行流的,线程是进程中的
  • 一个进程想要执行任务,必须至少有一条线程。应用程序启动的时候,系统会默认开启一条线程,也就是主线程。
  • 进程和线程的关系:
  • 线程是进程的执行单元,进程的所有任务都在线程中执行。
  • 线程是分配资源和调度的最小单位
  • 一个程序可对应多个进程(多进程);一个进程中可对应多个线程,但至少要有一条线程。
  • 同个进程内的线程共享进程资源。
  • 多进程:
  • 进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。显然程序是死的(静态的),进程是活动的(动态的)。
  • 进程可以分为系统进程用户进程
  • 系统进程:凡是用于完成操作系统的各种功能的进程就是系统进程,他们就是出于运行状态下的操作系统本身
  • 用户进程:运行用户程序时创建的运行在用户态下的进程。
  • 进程又被细化为线程,也就是一个进程下有多个能独立运行的更小的单位
  • 在同一个时间里,同一个操作系统中如果处于运行状态,这便是多进程
  • 多线程:
  • 同一时间, 只能处理1条线程,只有1条线程执行。多线程并发执行,其实是CPU快速地在多条线程之间调度(切换)。如果调度线程的时间足够快,就造成了多线程并发执行的假象
  • 如果线程非常至多(N条),会在这些(N条)线程之间调度,消耗大量的资源,每条线程被调用执行的频率会降低(线程的执行效率降低)。
  • 多线程的优点:
  • 适当提高程序的执行效率
  • 适当提高资源的利用率(CPU、内存利用率)
  • 多线程的缺点:
  • 开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512kb),如若开启大量线程,会占用大量的内存空间,就会降低程序的性能
  • 线程越多,CPU在调度线程的开销就越大
  • 程序设计更加复杂:如线程之间的通信、多线程之间的数据共享等

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

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

对比:

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

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

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

  1. 主线程队列: 可以调用来获得。因为是与主线程相关的,所以这是一个串行队列。和其它串行队列一样,这个队列中的任务一次只能执行一个。它能保证所有的任务都在主线程执行,而主线程是唯一可用于更新 UI 的线程。
  2. 串行队列(Serial Dispatch Queue):
    同一时间内,队列中只能执行一个任务,只有当前的任务执行完成之后,才能执行下一个任务。(只能开启一个线程,一个线程执行完毕后,再执行下一个任务)。主队列是主线程上的一个串行队列,是系统自动为程序创建的。
  3. 并行队列(Concurrent Dispatch Queue):
    同时允许多个任务同时执行。(可以开启多个线程,并且同时执行)。并发队列的并发功能只有在异步(dispatch_async) 函数下才有效

4、有哪些方法 ?

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

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

  1. dispatch_queue_create :
    创建队列
  2. Main Dispatch QueueGlobal Dispatch Queue
    系统提供的两种队列
  3. dispatch_set_target_queue :
    变更队列执行的优先级
  4. dispatch_after :
    延时执行
    的是函数并不是在指定时间后执行处理,而只是在指定时间追加处理到
  5. dispatch_group :
    调度任务组
  • :最后任务执行完的通知,比如:

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

  • 就如同它的名字一样,在队列执行的任务中增加“栅栏”,在增加“栅栏”之前已经开始执行的将会继续执行,当开始执行的时候其他的处于等待状态,的任务执行完后,其后的才会执行。
  1. dispatch_syncdispatch_async
  • dispatch_sync : 把任务同步追加到指定的中
  • dispatch_async :把任务异步追加到指定的中
  1. dispatch_apply
    会将一个指定的执行指定的次数。如果要对某个数组中的所有元素执行同样的的时候,这个函数就显得很有用了,用法很简单,指定执行的次数以及,在回调中会带一个索引,然后就可以根据这个索引来判断当前是对哪个元素进行操作:

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

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

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

  1. Dispatch Semaphore

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

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

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

  1. dispatch_once

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

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

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

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

  1. 主队列是主线中的一个串行队列
  2. 所有的和UI相关的操作(刷新或者点击按钮)都必须在主线程中的主队列中去执行,否则无法更新UI
  3. 每一个应用程序只有唯一的一个主队列用来update UI
    补充一点:如果在主线程中创建自定义队列(串行或者并行均可),在这个队列中执行同步任务,同样可以更新UI操作,主队列中可以更新UI,自定义队列也可以更新UI,但自定义队列的更新UI的前提是在主线程中执行同步任务。

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

  • 在某队列开启同步线程
  • +
  • 插队追加 操作同步
  • + 信号量锁
  • 串行NSOperationQueue队列并发数为1的时候 [NSOpertaion start] 启动任务即使同步操作 (NSOperationQueue.maxConcurrentOperationCount = 1)
  • 底层锁函数
  • 上层应用层封装的
  • 递归锁,这个锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中
  • & 条件锁
  • 同步操作 单位时间内只允许一个线程进入临界区
  • 单位时间内只允许一个线程进入临界区

7、 实现原理 ?

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

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

回归到代码就是

是的成员变量

有个内联函数

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

比较+交换 的原子操作。比较 的值是否等于

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

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

  • 死锁发生的四个必要条件是:
  1. 互斥条件(Mutual exclusion) :
    资源不能被共享,只能由一个进程使用
  2. 请求与保持条件(Hold and wait):
    进程已获得了一些资源,但因请求其它资源被阻塞时,对已获得的资源保持不放。
  3. 不可抢占条件(No pre-emption) :
    有些系统资源是不可抢占的,当某个进程已获得这种资源后,系统不能强行收回,只能由进程使用完时自己释放。
  4. 循环等待条件(Circular wait) :
    若干个进程形成环形链,每个都占用对方申请的下一个资源。
  • 一般死锁的应对策略有:
  1. 死锁预防:
    破坏导致死锁必要条件中的任意一个就可以预防死锁。例如,要求用户申请资源时一次性申请所需要的全部资源,这就破坏了保持和等待条件;将资源分层,得到上一层资源后,才能够申请下一层资源,它破坏了环路等待条件。预防通常会降低系统的效率。
  2. 死锁避免:
    避免是指进程在每次申请资源时判断这些操作是否安全。例如,使用银行家算法。死锁避免算法的执行会增加系统的开销。
  3. 死锁检测:
    死锁预防和避免都是事前措施,而死锁的检测则是判断系统是否处于死锁状态,如果是,则执行死锁解除策略。
  4. 死锁解除:
    这是与死锁检测结合使用的,它使用的方式就是剥夺。即:将某进程所拥有的资源强行收回,分配给其他的进程。
  • 死锁的避免:
  • 死锁的预防是通过破坏产生条件来阻止死锁的产生,但这种方法破坏了系统的并行性和并发性。
  • 死锁产生的前三个条件是死锁产生的必要条件,也就是说要产生死锁必须具备的条件,而不是存在这3个条件就一定产生死锁,那么只要在逻辑上回避了第四个条件就可以避免死锁
  • 避免死锁采用的是允许前三个条件存在,但通过合理的资源分配算法来确保永远不会形成环形等待的封闭进程链,从而避免死锁。该方法支持多个进程的并行执行,为了避免死锁,系统动态的确定是否分配一个资源给请求的进程。方法如下:
  • 如果一个进程的当前请求的资源会导致死锁,系统拒绝启动该进程;
  • 如果一个资源的分配会导致下一步的死锁,系统就拒绝本次的分配;
    显然要避免死锁,必须事先知道系统拥有的资源数量及其属性。

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

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

  • 琐是 纳秒 微秒级别
  • 参考自YY大神的不再安全的 OSSpinLock。单位是 ns 纳秒。
  • 锁相关的概念定义:
  1. 临界区:
    指的是一块对公共资源进行访问的代码,并非一种机制或是算法。
    每个进程中访问临界资源的那段程序称为临界区,每次只允许一个进程进入临界区,进入后不允许其他进程进入。
  2. 自旋锁:
    是用于多线程同步的一种锁,线程反复检查锁变量是否可用。
    由于线程在这一过程中保持执行,因此是一种忙等待
    一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。
    自旋锁,因此对于线程只会阻塞很短时间的场合是有效的。
  3. 互斥锁(Mutex):
    用于保护临界区,确保同一时间只有一个线程访问数据。 对共享资源的访问,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。
    互斥锁加锁失败而阻塞是由操作系统内核实现的,当加锁失败后,内核将线程置为睡眠状态;等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程加锁成功后就可以继续执行。
    性能开销成本,两次线程上下文切换的成本。
    当线程加锁失败时,内核将线程的状态从【运行】切换到,然后把CPU切换给其他线程运行;
    当锁被释放时,之前睡眠状态的线程会变成,然后内核就会在合适的时间把CPU切换给该线程运行
  4. 读写锁:
    是计算机程序的并发控制的一种同步机制,也称“共享-互斥锁”、多读者-单写者锁。用于解决多线程对公共资源读写问题。读操作可并发重入,写操作是互斥的。 读写锁通常用互斥锁、条件变量、信号量实现。
  5. 信号量(semaphore):
    是一种更高级的同步机制,互斥锁可以说是 在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。
  6. 条件锁:
    就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行。
  7. 递归锁(Recursive Lock):
    也称为,是互斥锁的一种,同一线程对其不会产生死锁。 递归锁会使用引用计数机制,以便可以从同一线程多次加锁、解锁,当加锁、解锁次数相等时,锁才可以被其他线程获取。

11、 中的 默认值

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

12、、、 的优劣?

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

  • 多线程可以访问同一个对象可分为情况处理:
  1. 如果只是只读,不用加锁。
  2. 如果只写的话,需要加锁。
  3. 如果需要的话,需要加锁(读写锁满足)。
    使用读写锁 。获取一个读写锁用于读称为,获取一个读写锁用于写称为独占锁,因此这种对于某个给定资源的共享访问也称为。
  • 多进程访问同一个对象
    一个程序几个进程在于这个程序的开发者的设置,可以是1个,也可以是多个的。
  1. 一个程序里有很多个进程
    一个程序几个进程在于这个程序的开发者的设置,可以是1个,也可以是多个的。一个应用程序,启动多个处理进程。换言之,所有进程隶属于当前应用程序;这是所谓的多进程服务
  2. 一个程序只有一个进程但被开启很多个
    启动多个同一应用程序,每个应用程序都是单进程。这个场景有些应用程序会禁用掉,有些是可以的,看应用程序的定位。如果允许,那么需要解决数据共享的问题(主要是数据写入);如果不允许,那么只能启动一个此类应用程序。
  • 我们就可以根据死锁的四个必要条件(), 使用死锁的四个应对策略()来解决死锁问题。
  • 我们也可以通过一些处理避免死锁
  1. 死锁的预防是通过破坏产生条件来阻止死锁的产生,但这种方法破坏了系统的并行性和并发性。
  2. 死锁产生的是死锁产生的,而不是,那么只要在
  3. 避免死锁采用的是,但通过的封闭进程链,从而避免死锁。该方法支持多个进程的并行执行,为了避免死锁,系统动态的确定是否分配一个资源给请求的进程。
  1. 如果一个进程的当前请求的资源会导致死锁,
  2. 如果一个资源的分配会导致下一步的死锁,

1、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、界面卡顿和检测你都是怎么处理?

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

卡顿优化:

  • 图片等大文件IO缓存
  • 耗时操作放入子线程
  • 高代码执行效率(JSON to Model的方案,锁的使用等,减少循环,UI布局frame子线程预计算)
  • UI减少全局刷新尽量使用局部刷新

监控卡帧:

  • 监控,结合子线程和信号量,两次事件触发时间间隔超过一个的时长,上报调用栈
  • 中添加监听,如果中间的耗时超过的时间,那么就是卡帧了,然后这个时候拿到线程调用栈,看看。那个部分耗时长即可。

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

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

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

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

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

  • 资源优化:
  • 删除无用图片
    使用 LSUnusedResources 查找无用图片。注意 [UIImage imageNamed:[NSString stringWithFormat:“icon_%d.png”,index]]; 这种使用图片的方式,可能会被误删。
  • 删除重复资源:Json、Plist、Extension 等
  • 压缩图片资源
  • 使用 ImageOptim 无损压缩图片。
  • 使用 TinyPNG 有损压缩图片。使用的时候直接执行 tinypng *.png -k token 脚本即可。
  • 其他技巧:
  • 用 LaunchScreen.storyboard 替换启动图片。
  • 本地大图片都使用 。
  • 资源按需加载,非必要资源都等到使用时再从服务端拉取。
  • 编译选项优化:
  • 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 字段的审核限制。当然其实跟安装包瘦身好像没有什么关系,所以除非快不行了否则不建议操作。
  • 苹果官方的策略:
  • 使用 管理图片
  • 开启

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

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

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

APP 启动分为热启动冷启动

  • 热启动是由于某种原因,APP的状态由running切换为suspend,但是此时APP并没有被系统kill掉,当我们再次把APP切换到前台的时候,APP会恢复之前的状态继续运行,这种就是热启动。我们平时所说的APP在后台的存活时间,其实就是APP能执行热启动的最大时间间隔。
  • 冷启动则是APP从被加载到内存到运行的状态。我们所说的启动优化一般是针对冷启动来说的。
  • 就苹果而言,它将启动分为两个阶段: 和 。启动时间也是针对这两个阶段进行优化,下面我们也将从这两方面进行优化:
  • pre-main 阶段优化:
    如图所示:
  • 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 :
    这个阶段基本不用优化。若 阶段优化很好,本阶段耗时也会很少
  4. initializer :
    在这个阶段,dylib 开始运行程序的初始化函数,调用每个类和分类的 方法,调用 C/C++ 中的构造器函数。 阶段执行结束后, dylib 开始调用 main() 函数。在这一步,检查 方法,尽量把事情推迟到 方法里执行;并且控制 category 数量,去掉不必要的 category。
    在这里我们修改了部分原本代码中直接在 +load 函数初始化逻辑改为在 +initialize 中加载也就是到使用时才加载。
  • main() 函数之后的优化:
  • didFinishLaunchingWithOptions 优化
  • 目前 App 的 方法里执行了多项项业务,有一大部分业务并不是一定要在这里执行的,如支付配置、客服配置、分享配置等。整理该方法里的业务,能延迟加载的就往后推迟,防止其影响启动时间。
  • 整理 ,将业务分级,对于非必须的业务移到首页显示后加载。同时,为了防止以后新加的业务继续往 里扔,可以新建一个类负责启动事件,新加的业务可以往这边添加。
  • 首页渲染优化
  • 减少启动期间创建的 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)模式。
    优点
  • 观察者模式可以实现表示层和数据逻辑层的分离,并定义了稳定的消息更新传递机制,抽象了更新接口,使得可以有各种各样不同的表示层作为具体观察者角色;
  • 观察者模式在观察目标和观察者之间建立一个抽象的耦合
  • 观察者模式支持广播通信;
  • 观察者模式符合开闭原则(对拓展开放,对修改关闭)的要求。

缺点

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

3、谈谈单例的优缺点?

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

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

  • MVC:
    MVC即 Model-VIew-Controller。他是1970年代被引入到软件设计大众的。MVC模式致力于关注点的切分,这意味着 model 和 controller 的逻辑是不与用户界面(View)挂钩的。因此,维护和测试程序变得更加简单容易。
    MVC设计模式将应用程序分离为3个主要的方面:Model,View和Controller
  • 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。
  • Model:Model层代表了描述业务逻辑和数据的一系列类的集合。它也定义了数据修改和操作的业务规则。
  • View:View代表了UI组件,像CSS,JQuery,html等。他只负责展示从 Presenter 接收到的数据。也就是把模型(译者注:非 Model 层模型)转化成UI。
  • Presenter:Presenter 负责处理 View 背后所有的UI事件。它通过 View 接收用户输入,之后利用 Model 来处理用户的数据,最后把结果返回给 View 。与 View 和 Controller 不同, View 和 Presenter 之间是完全解耦的,他们通过接口来交互。另外 Presenter 不像 Controller 处理进入的请求。

MVP模式关键点:

  1. 用户和 View 交互。
  2. View 和 Presenter 是一对一关系。意味着一个 Presenter 只映射一个 View 。
  3. View 持有 Presenter 的引用(译者注:应该是通过接口交互,并不直接引用Presenter),但是 View 不持有 Model 的引用(译者注:即使接口,也不会)。
  4. 在 View 和 Presenter 之间可以双向交互。
  • MVVM
    MVVM 即 Model-View-View Model。这个模式提供对 View 和 View Model 的双向数据绑定。这使得 View Model 的状态改变可以自动传递给View 。典型的情况是,View Model 通过使用 obsever 模式(观察者模式)来将 View Model 的变化通知给 Model。
  • 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注册()
    iOS系统中默认是支持 Url Scheme方式的,例如可以在浏览器中输入: weixin:// 就可以打开微信应用。自然在APP内部也可以通过这种方法来实现组件之间的路由设计。
    这种方式实现的原理是:在APP启动的时候,或者是向以下实例中的每个模块自己的 方法里面注册自己的断链(Url),以及对外提供服务(Block),通过url-scheme标记好,然后维护在url-router里面。 url-router中保存了各个组件对应的url-scheme,只要其它组件调用了 open url 的方法,url-router就会去根据url去查找对应的服务并执行。
  • URL 的命名规范
    遵循网上的 ( 模式的资源通用表示方式)的格式。例如 :
  • 常见的案例
  • JLRouters 本质可以理解为保存一个全局的,是,是对应存放的数组,和都会常驻在内存中,当打开一个时,就可以遍历这个全局的,通过来执行对应的。
  • MGJRouter的技术团队开源的一个,特点是使用简单方便的问题主要在于查找url的实现不够高效,通过遍历而不是匹配,还有就是功能偏多查找是基于匹配,所以会更高效,也是采用的这种方法, 绑定地过于紧密,一定程度上降低了灵活性。于是就有了 , 从数据结构上看它和 是一样的。
    蘑菇街方案不好的地方
  1. 对于实施组件化是完全没有必要的,拓展性和可维护性都降低;
  2. 基于 的方案的话,有一个致命缺陷:非常规对象无法参与本地组件间调度;但是可以通过传递来解决,但是这个区分了远程调用和本地调用的接口
  3. 模块内部是否仍然需要使用去完成调度?是没有必要的,为啥要复杂化?
  4. 当组件多起来的时候,需要提供一个关乎和服务的对应表,并且需要开发人员对这样一个表进行维护;
  5. 这种方式需要在APP启动时,每个组件需要到路由管理中心注册自己的及服务,因此内存中需要保存这样一份表,当组件多起来以后就会出现一些内存的问题;
  6. 混淆了,它们的处理逻辑是不同的。应该是把远程调用通过一个中间层转化成本地调用,如果把两者混为一谈,后期可能会出现无法区分业务的情况。比如对于组件无法响应的问题,远程调用可能直接显示一个,但是本地调用可能需要做其它处理。如果不加以区分,那么就无法完成这种业务要求。 远程调用只能传递被序列化的数据,像这样非常规的对象是不行的,所以如果组件接口要考虑远程调用,这里的参数与就不能是这类非常规对象。
  • routable-ios
  • HHRouter
  1. 优缺点:
    优点:
  • 是借鉴 和 得出来的解决方案。所以不管是H5、RN、Android、iOS 都通用。
  • 服务器可以动态的控制页面的跳转,可以统一页面出问题后错误处理,三端统一。

缺点:

  • 的规则是需要注册的,它们会在方法里面写。写在方法里面是会影响App启动速度的。
  • 大量的硬编码。链接里面关于都是硬编码,也都是硬编码。而且每个参数字段都必须要一个文档进行维护,这个对于业务开发人员也是一个负担。而且URL短连接散落在整个App四处,维护起来实在有点麻烦。
  • 对于传递的参数,是不够友好的,它最多是传递一个字典。
  1. 利用实现的方式()- 个人推荐
    相较于 的方式进行组件间的路由, 的方式利用了 的特征,实现了组件间服务的自动发现,无需注册即可实现组建间的调用。因此,不管从来说,都是一个比较完美的方案。
    的原理:
    传统的 。这个中间件 会依赖其他组件,其他组件也会依赖 。
    但是能不能让 不在依赖组件,各个组件之间不再依赖,组件间调用只依赖中间件 呢 ?
    官方 的优化建议是这样的:
    利用 的方式,创建一个 的类,类中定义了一些 方法,这些方法的结果是返回一个 或其他 。再给中间件 添加一个分类方法(),定义组件外部可调用的方法接口,内部实现 的方法。该方法主要通过 中的 获取 类和 获取方法名,这样就可以执行先去创建的 类中的方法得到返回值,在通过分类中的方法传值。
    优缺点:
    优点:
  • 充分的利用的特性,无需注册这一步。方案只有存在组件依赖这一层依赖关系。在中维护针对的,每个对应一个,中的方法对应场景。方案也统一了所有组件间调用入口。
  • 有一定的安全保证,它对中进行前缀进行验证

缺点:

  • 在中将常规参数打包成字典,在处再把字典拆包成常规参数,这就造成了一部分的硬编码
  1. 注册
    通过绑定,核心思想和代理传值是一样的,遵循协议,实现协议中的方法。
    主要思路:
  • 创建一个头文件 ,里面存放各个模块提供的协议。在各个模块依赖这个头文件,实现协议的方法。
  • 创建一个中间类 , 提供模块的注册和获取模块的功能(其实就是将类和协议名进行绑定,放在一个字典里,是协议名字符串,是类)。
  • 在各个模块中实现,核心代码如下:

优缺点:
优点:

  • 这个方案没有硬编码。

缺点:

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

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

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

  1. 开发过程:
  • 开发规范
  • 代码规范
  • 自测习惯
  • XMind、PDMan、PostMan、Jenkins、Sonar 等工具使用
  • Git 、Svn 、禅道、TAPD等使用规范
  • FPS 监控 :
  • CPU 使用率 :
  • 内存 : 来查看 、代码方面:Delegate、Block、 Block、 NSNotification
  • 启动时间: 优化
  • 耗电要求
  1. 代码检查:
  • CodeReview 习惯
  • 代码检查: OCLint、SwiftLint 、Sonar 等
  1. 测试:
  • 单元测试
  • UI 测试
  • 功能测试
  • 异常测试
  1. 线上:
  • 监控(日志系统):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,这样埋点代码也就得不到更新了
  • 所有前端埋点方案都会面临的数据传输时效性和可靠性的问题了,这个问题就只能通过在后端收集数据来解决了
  • 自动化埋点(无埋点)
  • 无埋点是指开发人员集成采集 后, 便直接开始捕捉和监测用户在应用里的所有行为,并全部上报,不需要开发人员添加额外代码;或者是说用户展现界面元素时,通过控件绑定触发事件,事件被触发的时候系统会有相应的接口让开发者处理这些行为。现在市面上:一种是预先跟踪所有的渲染信息,一种是滞后跟踪的渲染信息。
  • 数据分析师/数据产品通过管理后台的圈选功能来选出自己关注的用户行为,并给出事件命名。之后就可以结合时间属性、用户属性、事件进行分析了。所以无埋点并不是真的不用埋点了。
  • 优点:
  1. 由于采集的是全量数据,所以产品迭代过程中是不需要关注埋点逻辑的,也不会出现漏埋、误埋等现象
  2. 无埋点方式因为收集的是全量数据,可以大大减少运营和产品的试错成本,试错的可能性高了,可以带来更多启发性的信息
  3. 无需埋点,方便快捷
  4. 减少了因为人员流动带来的沟通成本
  5. 无需开发,业务人员埋点即可
  6. 支持先上报数据,后进行埋点
  • 缺点:
  1. 缺点与可视化埋点相同,未解决个性化自定义获取数据的问题,缺乏数据获取的灵活性
  2. 企业针对SDK开发难度较大,一般由数据分析企业研发提供,使用第三方提供的埋点方案,有如下缺陷:
    a、数据源丢失,应用上报的数据上传至第三方服务端,可能造成
    企业泄密或用户的关键数据丢失;

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

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

9、如何设计一个 git diff

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

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

  • 架构。
  • 优点:
  1. 耦合性低
    视图层和业务层分离,这样就允许更改视图层代码而不用重新编译模型和控制器代码,同样,一个应用的业务流程或者业务规则的改变只需要改动的模型层即可。因为模型与控制器和视图相分离,所以很容易改变应用程序的数据层和业务规则。
  2. 重用性高
    模式允许使用各种不同样式的视图来访问同一个服务器端的代码,因为多个视图能共享一个模型,它包括任何WEB(HTTP)浏览器或者无线浏览器(wap),比如,用户可以通过电脑也可通过手机来订购某样产品,虽然订购的方式不一样,但处理订购产品的方式是一样的。由于模型返回的数据没有进行格式化,所以同样的构件能被不同的界面使用。
  3. 部署快,生命周期成本低
    MVC使开发和维护用户接口的技术含量降低。使用MVC模式使开发时间得到相当大的缩减,它使程序员(Java开发人员)集中精力于业务逻辑,界面程序员(HTML和JSP开发人员)集中精力于表现形式上。
  4. 可维护性高
    分离视图层和业务逻辑层也使得WEB应用更易于维护和修改。
  • 缺点:
  1. 完全理解MVC比较复杂
    由于MVC模式提出的时间不长,加上同学们的实践经验不足,所以完全理解并掌握MVC不是一个很容易的过程。
  2. 调试困难
    因为模型和视图要严格的分离,这样也给调试应用程序带来了一定的困难,每个构件在使用之前都需要经过彻底的测试。
  3. 不适合小型,中等规模的应用程序
    在一个中小型的应用程序中,强制性的使用MVC进行开发,往往会花费大量时间,并且不能体现MVC的优势,同时会使开发变得繁琐。
  4. 增加系统结构和实现的复杂性
    对于简单的界面,严格遵循MVC,使模型、视图与控制器分离,会增加结构的复杂性,并可能产生过多的更新操作,降低运行效率。
  5. 视图与控制器间的过于紧密的连接并且降低了视图对模型数据的访问
    视图与控制器是相互分离,但却是联系紧密的部件,视图没有控制器的存在,其应用是很有限的,反之亦然,这样就妨碍了他们的独立重用。

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

  • 是苹果官方推荐的项目架构,相对于 、 架构来说入门相对的低一些;而且公司的项目不是很大,在综合人力成本等方面选择了 架构。
  • 针对 臃肿问题作出优化,将数据相关进行抽离管理,向 模式靠拢。

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

  • 组织架构:

    :负责维持图片的下载队列;
    :负责真正的图片下载请求;
    :负责图片的缓存;
    :是总的管理类,维护了一个 实例和一个 实例,是下载与缓存的桥梁;
    :负责图片的解压缩;
    :负责图片的预取;
    :和其他的扩展都是与用户直接打交道的。
    图片加载流程
  1. 判断图片URL 是否为 nil,是则做出错处理并返回;
  2. URL MD5加密生成 key;
  3. 根据 key 读取内存(memory)缓存, 有则拿到图片返回,否则往下;
  4. 根据 key 读取磁盘(disk)缓存,有则拿到图片返回,否则往下;
  5. 根据URL 下载图片,下载成功则将图片保存到 内存和磁盘中返回图片
  • 组织架构:主要有个模块
  1. :是对 的封装,负责发送网络请求,是 中使用最多一个模块
  2. :实时监测网络状态的工具类
  3. :网络安全策略的工具类,主要是针对于 Https 服务
  4. :请求序列化工具类
  • :请求入参序列化工具基类
  • :请求回参序列化工具基类
  • : 解析器, 的默认解析器
  • : 解析器
  • : 万能解析器,直接返回二进制数据(),服务器不会对数据进行处理
  1. : 对iOS 的扩展
  • 的可能面试考点
  1. 2.x怎么开启常驻子线程?为何需要常驻子线程?
    在 版本中 通过 开启了一个常驻子线程,具体代码是这样的:
  1. 为何要开启常驻子线程?
    的接口是异步的,然后会在发起的线程回调。而一个子线程,在同步代码执行完成之后,一般情况下,线程就退出了。那么想要接收到 的回调,就必须让子线程至少存活到回调的时机。而AF让线程常驻的原因是,当发起多个请求的时候,会统一在这个子线程进行回调的处理,所以干脆就让其一直存活下来
    上面说的一般情况,子线程执行完任务就会退出。子线程能够继续存活,就需要通过 来开启常驻线程。
  2. 的关系,每次都需要新建 吗?
    中 与 是1对1的关系, 会在 初始化的时候创建对应的 。同样, 也在注释中写明了可以提供一个配置好的 单例来全局复用。
    这里复用 其实就是在利用 多路复用特点,减少访问同一个服务器时,重新建立 连接的耗时和资源。
  3. 如何避免中间人攻击?
    现在,由于苹果,基本都切到 了, 的基本原理还是需要了解一下的,这里不做介绍。
    通常,首先我们要了解中间人攻击,大体就是黑客通过截获服务器返回的证书,并伪造成自己的证书,通常我们使用的 等工具实际上就可以看成中间人攻击。
    解决方案其实也很简单,就是 。 的 就是相关设置项。
    的原理就是需要将服务器的公钥打包到客户端中, 验证时,会将服务器的证书和本地的证书做一个对比,一致的话才允许验证通过。
  1. 由于数字证书存在有效期,内置到客户端后就存在失效后导致验证失败的问题,所以可以考虑设置为 的模式,这样的话,只要保证证书续期后,证书中的公钥不变,就能够通过验证了。
  2. 为什么不再需要常驻线程?
    使用 ,痛点就是:发起请求后,这条线程并不能随风而去,而需要一直处于等待回调的状态。所以 在权衡之后选择了常驻线程。
    之后使用了 :
  1. 使用 解决了 的痛点,从上面的代码可以看出, 发起的请求,不再需要在当前线程进行代理方法的回调。可以指定回调的 ,这样我们就不用为了等待代理回调方法而苦苦保活线程了
    指定的用于接收回调的 设为了 ,这里
  1. 这边对 的访问进行了加锁,是保证多线程环境下的数据安全。既然加了锁,就算 不设为,当某个请求正在回调时,下一个请求还是得等待一直到上个请求获取完所要的资源后解锁,所以这边并发回调也是没有意义的。相反多 回调导致的多线程并发,还会导致性能的浪费。所以 。

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

  • 重构技巧:
  1. 重复代码的抽象提炼
  2. 冗长方法的分隔
  3. 嵌套条件分支的优化
  4. 去掉一次性的零时变量
  5. 消除过长参数列表
  6. 提取类或继承体系中的常量
  7. 让类提供应该提供的方法
  8. 拆分冗长的类
  9. 提取继承体系中重复的属性与方法到父类
  • 适合节点:
  1. 【增】在增加新功能的时候(增加新功能的时候,发现需要重构来便于新功能的添加)
  2. 【删】在扩展不再简单的时候(消除重复)
  3. 【改】修复缺陷(修复 的时候)
  4. 【查】代码审查(通过交流提出了很多修改的主意)

重构是一个不断的过程。

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

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

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

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

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


  1. 数据元素,每个结点只有一个元素。存储位置反映数据元素间的逻辑关系
  • ,但是插入、删除操作效率较差。(比如::1-2-3-4-5-6-7-8-9-10,存储是按顺序的。再比如和等)。

  1. 每个结点除了包含数据元素信息外还包含反映数据元素间的逻辑关系
  • 这种存储方式不要求存储空间连续,便于进行插入和删除操作,但是存储空间利用率较低
  • 另外,由于逻辑上相邻的数据元素在存储空间上不一定相邻,所以不能对其进行随机存取

  1. 通过解决冲突的方法,将关键字在地址空间内,并将哈希函数的值作为该数据元素的存储地址。
  • 其特点是存取速度快只能按关键字随机存取不能顺序窜出也不能折半存取

  1. 索引存储除了数据元素存储在一外,尚需建立一个。索引表中的索引指示结点的存储位置,兼有动态和静态的特性。

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

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

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

  • 链表:
    是一种物理存储单元上、的存储结构数据元素的是通过链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分
  1. 一个是存储数据元素的数据域。
  2. 另一个是存储下一个结点地址的指针域。

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

  • 单向链表:
    A->B->C->D->E->F->G->H。 这就是单向链表 ,H 是头 A 是尾,像一个只有一个头的火车一样。只能一个头拉着跑。
  • 双向链表:
    H<- A->B->C->D->E->F->G->H。 这就是双向链表有头没尾,两边都可以跑 ,跟地铁一样 到头了,可以倒着开回来。
  • 循环链表:
    A->B->C->D->E->F->G->H->A,绕成一个圈就像蛇吃自己的这就是循环。

4、数组和链表区别

  • 数组是可以在内存中连续存储多个元素的结构,在内存中的分配也是连续的,数组中的元素通过数组下标进行访问,数组下标从0开始
    优点:
  1. 按照索引查询元素速度快
  2. 按照索引遍历数组方便

缺点:

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

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

  • 链表:
    是一种物理存储单元上、的存储结构数据元素的是通过链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分
  1. 一个是存储数据元素的数据域。
  2. 另一个是存储下一个结点地址的指针域。

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

5、堆、栈和队列

  • 堆是在程序运行时,而不是在程序编译时,申请某个大小的内存空间。即 ,对其访问和对一般内存的访问没有区别。堆是指程序运行时申请的动态内存,而栈只是指一种使用堆的方法(即先进后出)。
  • 是一种比较特殊的数据结构,可以被看做一棵树的数组对象,具有以下的性质:
  1. 堆中某个节点的值总是不大于或不小于其父节点的值;
  2. 堆总是一棵完全二叉树。
  • 堆分为两种情况,有
    将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆,在一个摆放好元素的最小堆中,父结点中的元素一定比子结点的元素要小,但对于左右结点的大小则没有规定谁大谁小。
  • 堆常用来实现,堆的存取是随意的,这就如同我们在图书馆的书架上取书,虽然书的摆放是有顺序的,但是我们想取任意一本时不必像栈一样,先取出前面所有的书,书架这种机制不同于箱子,我们可以直接取出我们想要的书。
  • 栈:
  • 栈是一种具有的数据结构,又称为,简称 (—First-In/Last-Out)结构。也就是说后存放的先取,先存放的后取,这就类似于我们要在取放在箱子底部的东西(放进去比较早的物体),我们首先要移开压在它上面的物体(放进去比较晚的物体)。
  • 栈是限定仅在的线性表。我们把允许插入和删除的一端称为栈顶,另一端称为栈底,不含任何数据元素的栈称为空栈。栈的特殊之处在于它限制了这个线性表的插入和删除位置,它始终只在栈顶进行。
  • 堆栈中定义了一些操作。两个最重要的是和。在堆栈的顶部加入一个元素。相反,在堆栈顶部移去一个元素,并将堆栈的大小减一。
  • 系统会给栈。
  • 常用:
  1. 递归(如:逆序输出)
  2. 语法检查,符号成对出现
  3. 数制转换
  4. 二叉树的一些操作
  • 队列:
  • 队列是一种(—first in first out)的数据结构,又称为先进先出的线性表,简称 FIFO(First In First Out)结构。也就是说先放的先取,后放的后取,就如同行李过安检的时候,先放进去的行李在另一端总是先出来,后放入的行李会在最后面出来。
  • 队列是的线性表。允许插入的一端称为队尾,允许删除的一端称为队头。它是一种特殊的线性表,在于它只允许在表的前端进行删除操作,而在表的后端进行插入操作(队头做删除,队尾做插入)。和栈一样,队列是一种操作受限制的线性表。

6、二叉树相关操作

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

  1. 如果一棵树只有一个结点,它的深度为1。
  2. 如果根结点只有左子树而没有右子树, 那么树的深度应该是其左子树的深度加1。
  3. 同样如果根结点只有右子树而没有左子树,那么树的深度应该是其右子树的深度加1。
  4. 如果既有右子树又有左子树, 那该树的深度就是其左、右子树深度的较大值再加1。

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

  • 重复遍历结点
  1. 先求出根结点的左右子树的深度;
  2. 然后判断它们的深度相差不超过1,如果否,则不是一棵二叉树;
  3. 如果是,再用同样的方法分别判断左子树和右子树是否为平衡二叉树,如果都是,则这就是一棵平衡二叉树。
  • 遍历一遍结点
    遍历结点的同时记录下该结点的深度,避免重复访问。
  • 方法:

1、时间复杂度

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

2、空间复杂度

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

3、常用的排序算法

  1. 冒泡排序:
  • 原理:就是重复地走访过要排序的元素列,依次比较两个相邻的元素,顺序不对就交换,直至没有相邻元素需要交换,也就是排序完成。
  • 这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。
  • 冒泡排序是一种算法。
  • 时间复杂度:最好情况(初始情况就是正序)下是o(n),平均情况是o(n²)
  1. 选择排序
  • 选择排序(Selection sort)是一种简单直观的排序算法。原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素存放在序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到全部待排序的数据元素排完。
  • 选择排序是的排序方法
  • 时间复杂度:最好和平均情况下都是O(n²)
  1. 直接插入排序
  • 插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序,
  • 插入排序的基本思想是:每步将一个待排序的记录,按其关键码值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止
  • 直接插入排序是算法。
  • 时间复杂度:最好情况(初始情况就是正序)下是o(n),平均情况是o(n²)
  1. 二分插入排序
  • 由于在插入排序过程中,待插入数据左边的序列总是有序的,针对有序序列,就可以用二分法去插入数据了,也就是二分插入排序法。适用于数据量比较大的情况。
  • 二分插入排序的算法思想:
    算法的基本过程:
    (1)计算 0 ~ i-1 的中间点,用 i 索引处的元素与中间值进行比较,如果 i 索引处的元素大,说明要插入的这个元素应该在中间值和刚加入i索引之间,反之,就是在刚开始的位置到中间值的位置,这样很简单的完成了折半**;
    (2)在相应的半个范围里面找插入的位置时,不断的用(1)步骤缩小范围不停的折半,范围依次缩小为 1/2 1/4 1/8 …快速的确定出第 i 个元素要插在什么地方;
    (3)确定位置之后,将整个序列后移,并将元素插入到相应位置
  • 二分插入排序是的排序算法。
  • 时间复杂度:最好情况(刚好插入位置为二分位置)下是O(log₂n),平均情况和最坏情况是o(n²)
  1. 希尔排序
  • 希尔排序(Shell’s Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。
  • 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,排序完成。
  • 希尔排序是排序算法。
  • 时间复杂度:O(n^(1.3—2))
  1. 快速排序
  • 快速排序(Quicksort)是对冒泡排序的一种改进
  • 它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
  • 快速排序是的排序算法
  • 时间复杂度:最差为O(n^2),平均为O(nlogn),最好为O(nlogn)
  1. 堆排序
  • 是指利用堆这种数据结构所设计的一种排序算法堆是一个近似完全二叉树的结构并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点
  • 在堆的数据结构中,堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。堆中定义以下几种操作:
  • 最大堆调整(Max Heapify): 将堆的末端子节点作调整,使得子节点永远小于父节点
  • 创建最大堆(Build Max Heap): 将堆中的所有数据重新排序
  • 堆排序(HeapSort): 移除位在第一个数据的根节点,并做最大堆调整的递归运算
  • 堆排序是一个非稳定的排序算法。
  • 时间复杂度:O(nlogn)

4、字符串反转

5、链表反转(头差法)

  • :
    将链表每个节点依次取下来头插到新链表,即为原链表的反转;因为改变了当前节点的 next 指向,必须先保存 next 地址。

  • 遍历列表时,将当前节点的 next 指针改为指向前一个元素。由于节点没有引用其上一个节点,因此必须事先存储其前一个元素。在更改引用之前,还需要另一个指针来存储下一个节点。不要忘记在最后返回新的头引用。

6、有序数组合并

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

两个思路:

  1. hash 不同编译器对字符数据的处理不一样,所以hash之前先把字符类型转成无符号类型;
  2. 空间换时间,用buffer数组记录当前只找到一次的字符,避免二次遍历。

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

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

  • 方法1:
  • 方法2:
    方法一明显的是两层for循环,时间复杂度为 O(N^2) 一个改进的办法:我们将一个路径中的所有点先放进NSSet中.因为NSSet的内部实现是一个hash表,所以查询元素的时间的复杂度变成 O(1),我们一共有N个节点,所以总时间复杂度优化到了O(N)

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

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

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

  • 第一层for循环从索引0到倒数第二个索引拿到每个数组元素,
  • 第二个for循环遍历上一层for循环拿到的元素的后面的所有元素。

1、谈谈对 HTTP、HTTPS 的理解

  • 超文本传输协议,他是基于TCP应用层协议
  • 无连接 无状态 的,需要通过cookies 或者 session 来保持会话
  • HTTP 分为部分:请求报文和响应报文
  • 请求报文个部分组成:、、、
  • 请求报文个部分组成:、、、
  • URL 构成:
  • 协议构成:
    请求行、请求头、请求体
  • 常用的请求方式?
    答: GET、POST、PUT、DELETE、HEAD、OPTIONS?
  • GET 和 POST 的区别?
  • GET 把参数通过 & 拼接在URL 后面,POST 放在 body 里面
  • GET 有长度限制(一般2048字符),POST没有限制
  • 由于参数的存放,POST 相对于 GET 安全,相对安全是因为 POST 仍可以被抓包
  • GET 是可以被缓存的,POST 不可被缓存(后台使用Redis、Memcached 登记室除外)
  • :
    HTTPS是一种通过计算机网络进行安全通信的传输协议(以安全为目标),经由HTTP进行通信,利用SSL/TLS建立全信道,加密数据包。HTTPS使用的是提供对网站服务器的身份认证,同时保护交换数据的隐私与完整性(和保证了传输过程的安全性)。
    是传输层加密协议,前身是。HTTPS 的安全基础
    通过抓包可以看到数据不是明文传输,而且HTTPS有如下特点:
  • HTTPS特点:
    基于HTTP协议,通过SSL或TLS提供加密处理数据、验证对方身份以及数据完整性保护

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

    非对称加密过程需要用到公钥进行加密,那么公钥从何而来?其实公钥就被包含在数字证书中,数字证书通常来说是由受信任的数字证书颁发机构CA,在验证服务器身份后颁发,证书中包含了一个密钥对(公钥和私钥)和所有者识别信息。数字证书被放到服务端,具有服务器身份验证和数据传输加密功能。
  • HTTPS 的验证流程?
    归纳为个步骤:
  1. 客户端发起一个http请求,告诉服务器自己。
  2. 服务端把自己的信息以数字证书的形式返回给客户端(证书内容有,,,等)。证书中有一个公钥来加密信息私钥由服务器持有
  3. 验证证书的合法性:
    客户端收到服务器的响应后会先验证证书的(证书中包含的地址与正在访问的地址是否一致,证书是否过期)。
  4. 生成随机密码(RSA签名):
    如果验证通过,或用户接受了不受信任的证书,客户端就会生成一个(session key)并用,让服务端用私钥解密,解密后就用这个对称密钥进行传输了,并且能够说明服务端确实是私钥的持有者。
  5. 生成对称加密算法:
    验证完服务端身份后,客户端生成一个,以公钥加密之后发送给服务端。此时被黑客截获也没用,因为只有服务端的私钥才可以对其进行解密。之后客户端与服务端可以用这个对称加密算法来加密和解密通信内容了。
  • 数字证书都有哪些内容?
  1. – 证书的发布机构
    发布证书的机构,指明证书是哪个公司创建的(并不是指使用证书的公司)。出了问题具体的颁发机构是要负责的。
  2. – 证书的有效期
    证书的使用期限。过了这个期限证书就会作废,不能使用。
  3. – 公钥
    通常是一个,进行加密/解密算法时使用。公钥和私钥都是密钥,只不过一般公钥是的,加密时使用;私钥是的,解密时使用。
  4. – 主题
    证书是颁发给谁了,一般是个人或公司名称机构名称公司网站的网址
  5. – 签名所使用的算法
    数字证书的数字签名所使用的加密算法,根据这个算法可以对。指纹加密的结果就是数字签名
  6. – 指纹以及指纹算法(一种HASH算法
    指纹和指纹算法会和证书放在一起
    用来保证证书的完整性,确保证书没有修改过。
    使用者在打开证书时根据。
  • 客户端如何检测数字证书是并是所要请求的公司的?
  1. 首先应用程序读取证书中的 (发布机构),然后会在 发布机构中去找该机构的证书(为什么操作系统会有受信任机构的证书?先看完这个流程再来回答)。
  2. 如果找不到就说明证书是水货,证书有问题,程序给错误信息。
  3. 如果找到了,或用户确认使用该证书。就会拿上级证书的,解密本级证书,得到数字指纹。然后对本级证书的公钥进行数字摘要算法(证书中提供的指纹加密算法)计算结果,与解密得到的指纹对比。如果一样,说明证书没有被修改过。公钥可以放心使用,可以开始握手通信了。
  • 操作系统为什么会有证书发布机构的证书?
  1. 证书发布机构除了给别人发布证书外,自己也有自己的证书
  2. 在操作系统安装好时,受信任的证书发布机构的数字证书就已经被安装在操作系统中了,根据一些权威安全机构的评估,选取一些信誉很好并且通过一定安全认证的证书发布机构,把这些证书默认安装在操作系统中并设为信任的数字证书。
  3. 发布机构持有与自己数字证书对应的私钥,会用这个私钥加密所有他发布的证书及指纹整体作为数字签名。

2、TCP、UDP 和 Socket

  • TCP:(Transmission Control Protocol )传输控制协议,是一种面向连接的可靠的基于字节流传输层通信协议
  • 三次握手
  1. 客户端发送 SYN(SEQ=x)报文给服务器端,进入 SYN_SEND 状态。
  2. 服务器端收到 SYN 报文,回应一个 SYN (SEQ=y)ACK(ACK=x+1)报文,进入 SYN_RECV 状态。
  3. 客户端收到服务器端的 SYN 报文,回应一个 ACK(ACK=y+1) 报文,进入 Established 状态。
  • TCP 为什么要三次握手?而不是两次或者四次呢?
  1. 为了, TCP 协议的通信双方, 都必须维护一个序列号, 以标识发送出去的数据包中, 哪些是已经被对方收到的。 三次握手的过程即是通信双方相互告知序列号起始值, 并确认对方已经收到了序列号起始值的必经步骤。
  2. 如果只是两次握手, 至多只有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认。
  3. 如果是四次或者其他,多于三次则是累赘,因为三次已经可以确保。
  • 四次挥手:
  1. 某个应用进程首先调用 close,称该端执行“主动关闭”(active close)。该端的 TCP 于是发送一个 FIN 分节,表示数据发送完毕。
  2. 接收到这个 FIN 的对端执行 “被动关闭”(passive close),这个 FIN 由 TCP 确认
  3. 一段时间后,接收到这个文件结束符的应用进程将调用 close 关闭它的套接字。这导致它的 TCP 也发送一个 FIN
  4. 接收这个最终FIN的原发送端 TCP(即执行主动关闭的那一端)确认这个 FIN
    既然每个方向都需要一个 FIN 和一个 ACK,因此通常需要4个分节。
  • UDP:(User Datagram Protocol)用户数据报协议,是一种高速传输和实时性有较高的无连接的不可靠的 传输层协议
  • TCPUDP 的区别?
    1、连接性:TCP 面向连接,UDP 无连接
    2、可靠性:TCP 可靠的、保证消息顺序,UDP 不可靠(易丢包)、不能保证顺序
    3、模式:TCP 流模式,UDP 数据报格式
    4、资源损耗:TCP 更损耗数据
  • Socket:socket 是 “open—write/read—close” 模式的一种实现,那么socket 就提供了 这些操作对应的函数接口。使用socket 需要注意:
  • 心跳的保持
  • ping 和 pong 的呼应
  • 离开页面要断开,进入页面再重新连接

1、多态

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

  1. 动态类型:
    编译器编译的时候是不能被识别的(如 id 类型),要等到运行时(run time),即程序运行的时候才会根据语境来识别。所以这里面就有两个概念要分清:编译时运行时
  2. 动态绑定 :
    动态绑定(dynamic binding)貌似比较难记忆,但事实上很简单,只需记住关键词/即可。
    而在OC中,其实是没有函数的概念的,我们叫,所谓的函数调用就是给对象发送一条消息。这时,动态绑定的特性就来了。OC可以先跳过编译,到运行的时候才动态地添加函数调用,在运行时才决定要调用什么方法,需要传什么参数进去。这就是动态绑定,要实现他就必须用SEL变量绑定一个方法,最终形成的这个SEL变量就代表一个方法的引用。动态绑定的特定不仅方便,而且效率更高。
  3. 动态加载 :
    让程序在运行时添加代码模块以及其他资源。用户可以根据需要加载一些可执行代码和资源,而不是在启动时就加载所有组件。可执行代码中可以含有和程序运行时整合的新类。

2、继承

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

4、代理(Delegate)


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

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

5、通知(NSNotificationCenter)

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

  • 如果实现通知机制?

6、KVC (Key-value Coding)

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

  • KVC 底层实现原理:
    当一个对象调用方法时,方法内部会做以下操作:
  1. 判断有没有指定 的 ,如果有方法,就会调用 方法,给该属性赋值
  2. 如果没有 方法,判断有没有跟 值相同且带有下划线的成员属性 如果有,直接给该成员属性进行赋值
  3. 如果没有成员属性 ,判断有没有跟 相同名称的属性。如果有,直接给该属性进行赋值
  4. 如果都没有,就会调用 和 方法
  • 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,不会造成野指针。比如自定义 控件属性也是用 weak (因为父控件的 subViews 数组已经对它有了一次强引用)。
  • copy:分为深拷贝和浅拷贝
  • 浅拷贝:对内存地址的复制,让目标对象指针和原对象指向
  • 深拷贝:对对象内容的复制开辟新的内存空间
  • 可变对象的copy和mutableCopy都是深拷贝
    不可变对象的copy是浅拷贝,mutableCopy是深拷贝
    copy方法返回的都是不可变对象

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

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

9、@synthesize 、@dynamic 的区别

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

10、UIView 和 CALayer 的关系?

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

11、ViewController 不走 dealloc 的情况

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

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

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

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

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

14、NSCache 优于 NSDictionary 的几点?

是一个非常奇怪的集合。。这使它。它自动对做出反应并基于。与NSDictionary相比,键是被retain而不是被拷贝的。

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

15、 和 有什么区别?

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

16、self 和 super 的区别 ?

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

17、 和 两者是什么关系?

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

1、Swift 和 OC

  • 和 OC 的联系
  1. 和 OC 共用一套运行时环境, 和 OC 可以互相桥接,互相引用混合编程;
  2. OC 中很多类库,在 swift 中依然可以直接使用,只是语法上有些改变;
  3. OC 中的、、、、、、、、等绝大多数概念,在 中继续有效。
  4. 中有 OC 没有的一些概念。比如: , ,函数式编程模式(如 map、filter、reduce 等)等。
  • 相对于 OC 的优势
  1. 容易阅读,语法和文件结构简洁化。
  2. 更容易维护,文件分离后结构更清晰。
  3. 更加安全,它是的语言。
  4. 代码更少,语法更简洁,可以省去大量冗余的代码。
  5. 速度更快,运算性能更高。

2、Swift 的可选项类型()

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

3、Swift 中的 和

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

4、swift 中 defer、guard?

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

  • 当条件满足的时候,会顺序执行,如果 条件不满足的时候,会进入 内部,并执行 return 操作,终止代码的执行

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

  • 用于映射, 可以将一个列表转换为另一个列表。
    数组元素类型转换
  • 功能跟map类似; 区别是flatMap会过滤nil元素, 并解包。
    还可以将多维数组转换为一维数组,对于N维数组, map函数仍然返回N维数组。
  • 用于过滤, 可以筛选出想要的元素
  • 方法把数组元素组合计算为一个值。

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

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

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


  • 表示这个函数时可以的,也就是说执行这段代码是可以当前线程。
  1. 函数/方法可以是异步的,属性也可以是异步的
  2. 当您将函数标记为异步时,您就允许它挂起。当一个函数挂起自己时,它也会挂起它的调用者。所以它的调用者也必须是异步的。
  3. 为了指出异步函数中它可能挂起一次或多次的位置,使用了 await 关键字。
  4. 当异步函数被挂起时,线程不会被阻塞。
  5. 当异步函数恢复时,从它调用的异步函数返回的结果流回原始函数,并从上次停止的地方继续执行。

  • 在函数、属性和初始值设定项中, 可用于表达式可以解除当前线程阻塞;除此之外, 还可以用于异步序列。

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

Swift 中派发机制分为 三种。


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

  • 函数表派发是。
    函数表使用了一个 来存储类声明的每一个函数的指针。大部分语言把这个称为 “”(虚函数表),Swift 里称为 “”。,里面记录着类所有的函数,如果父类函数被 的话,表里面只会保存被 之后的函数。一个子类新添加的函数,都会被插入到这个数组的最后。运行时会根据这一个表去决定实际要被调用的函数。
  • : Object-c的OO实现
    消息机制是调用函数最动态的方式。也是 的基石,这样的机制催生了 , 和 等功能。这种运作方式的关键在于开发者可以在运行时改变函数的行为。不止可以通过 来改变,甚至可以用 修改对象的继承关系, 可以在面向对象的基础上实现自定义派发。

如图:做了总结

  • 派发的使用场景:
  • 使用直接派发。
  • 和使用的是直接派发。
  • 和使用的是函数表派发
  • 的 使用的是消息机制派发
  • 指定派发方式:
  1. 允许类里面的函数使用直接派发。这个修饰符会让函数失去动态性。任何函数都可以使用这个修饰符,就算是 里本来就是直接派发的函数。
  2. 这也会让 的运行时获取不到这个函数, 不会生成相应的 。

  • 可以让类里面的函数使用消息机制派发
  1. 使用 , 必须导入 框架,里面包括了 NSObject 和 Objective-C 的运行时。
  2. 可以让声明在 里面的函数能够被
  3. 可以用在所有 NSObject 的子类和 Swift 的原声类。这就是为什么的属性需要用修饰。

  • 函数能被 的运行时捕获到。
    使用 的典型例子就是给 一个命名空间 ,让这个函数可以被 Objective-C 的运行时调用。

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

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

库的,是的一个。根据链接方式不同,可以分为动态库和静态库,其中。

  • 静态库:
    静态库形式:,是在进行链接,从静态库文件中到最终的可执行文件中。
    被多次使用就有多份冗余拷贝。
  • 动态库:
    静态库形式: 和 ,,而是,当程序运行时需要的时候再获取(动态加载)。 系统,多个程序共用,节省内存。
  • 动静态库区别:

    以上就是本篇文章【iOS runloop 处理卡顿 ios runloop的使用场景】的全部内容了,欢迎阅览 ! 文章地址:http://keair.bhha.com.cn/news/4782.html 
     文章      相关文章      动态      同类文章      热门文章      栏目首页      网站地图      返回首页 康宝晨移动站 http://keair.bhha.com.cn/mobile/ , 查看更多   
最新文章
坪山区红酒电商打包推广,开启红酒销售新征程
在当今数字化时代,电商行业蓬勃发展,而红酒作为一种具有深厚文化底蕴和广泛消费群体的饮品,其电商销售也逐渐成为热门领域,坪
chatgpt赋能python:如何找到Python的位置?
在成为一名Python工程师之前,首先你需要知道如何找到Python的位置。Python通常作为一种解释性语言,在计算机上执行
百度推广调价助手(百度推广调价助手怎么用)
大家好,今天小编关注到一个比较有意思的话题,就是关于百度推广调价助手的问题,于是小编就整理了3个相关介绍百度推广调价助手
介绍吴桥SEO操作,如何让您的网站在搜索引擎中脱颖而出
随着互联网的飞速发展,搜索引擎优化(SEO)已成为企业提升网站排名、提高品牌知名度的重要手段。,作为我国著名的杂技之乡,拥
全部本地化!ChatGLM+oneapi+fastgpt+微秘+微信 部署实操
One-API接入ChatGLM2-6B-int4的OpenAI服务接口 参考:https://doc.fastgpt.in/docs/development/one-api/ 基于docker进行
7.我每个月结余下来500块来投.资有意义吗?
     【量化思维】是富人思维最重要的思维之一,从今天开始重视自己的时间价值,哪怕是目前一无所有的人࿰
手把手让你实现开源企业级web高并发解决方案
本来想起个比较风趣点的标题,可想来思去,还是走常规路线,做一系列的手把手吧。这样一来,便于我的老朋友们识别,也让我对这篇
iOS runloop 处理卡顿 ios runloop的使用场景
1、什么是 ?作用有哪些? 可以称之为运行循环,在程序运行过程中循环做一些事情,如果没有程序执行完毕就会立即退出,有程序会一
国外虚拟主机1美元能买吗?
国外虚拟主机1美元是可以买的,但是提供1美元商家并不多,而Hostens虚拟主机是其中一个,最低售价1.6美元/月,10GB磁盘空间、1TB
企业整站seo-整站seo服务
在当今数字化时代,企业整站 SEO 已成为提升网站竞争力和吸引潜在客户的关键策略,通过深入分析市场行情和搜集最新资讯,我们可
相关文章