常用知识点


设计模式


参见这里 设计模式

  • 创建型模式: 单例模式、工厂方法模式、抽象工厂模式、工厂方法参见这里
  • 结构性模式:代理模式、类簇、装饰模式、享元模式
  • 行为模式:观察者模式、命令模式
    可以参考这个 面试题

MVC 和 MVVM

mvc model:数据层,用来存储数据和处理数据的数据接口层。 view:负责页面的战士与用户交互。 controller:用来处理数据层和展示层的逻辑,当 view 有用户输入操作的时候,负责更新 model 的存储,当 model 只发生改变时,负责更新对应的view。缺点就是:controller 层比较厚重,逻辑太多,可测试性差。

mvvm:viewmodel:来处理view和model的业务逻辑,负责网络请求、负责数据缓存,使得UI和业务逻辑拆分开。

单例模式

一个类在应用中只有一个实例,控制了实例的个数,节约了资源,static声明全局变量,在整个进程运行期间只会被赋值一次,通过 getInstance 获取实例,重写 allocWithZone:防止外部通过 alloc 获取对象。系统的单例,比如 application、filemanager、userdefault、mainscreen。
缺点:实例对象在堆区,程序结束才会释放。单例类的职责过重,在一定程度上违背了”单一职责“的原则。单例类无法继承,因此很难进行类的扩展。

观察者模式

KVO

委托模式

delegate + 协议的组合,比如 UITableView、UITextField
好处:避免子类化带来的过多的子类以及子类与父类的耦合。通过委托传递消息,实现分层解耦。

修饰符相关

property

  1. @property 相当于 ivar(实例变量)+ getter + setter
  2. isa 为指针变量

关于weak和assign的区别

  1. 两者都是弱引用。
  2. weak 只可以来修饰对象,对象释放后,指针在ARC下会置为 nil ,向 nil 发送消息不会崩溃(引用计数器会置为0)
  3. assign 只能修饰 非引用型基本数据类型,诸如 int 等,是安全的会自动置为nil。但是如果用于修饰对象,则会产生野指针(引用计数器在释放后不会置为0),assign不会修改旧值和新值的引用计数器,因此对象被释放后,会形成野指针。

retain、strong、copy的区别

  1. retain是MRC时期的写法,strong 是ARC时期的写法。都是强引用,当修饰属性时时一样的。但是当修饰block时会很不一样,strong相当于 copy ,而 retain 相当于 assign。Block 要用 copy 修饰,block 在ARC模式下载 堆内。
  2. 而 mutableCopy 无论作用于可变还是非可变对象,都是内容的拷贝。
  3. strong 修饰的对象,无论有没有被加到VS上都会一直占用内存,只有在 dealloc 的时候才会被释放。而 weak 修饰的对象,如果没有被强引用,则会被系统自动释放掉。
  4. 对于可变对象 NSMutableString、NSMutableArray,不能使用 copy ,因为Foundation框架已经实现了 copying 协议,如果用 copy 修饰,只会返回一个不可变对象,而如果对不可变对象使用可变对象的操作,则会引起异常。所以用 strong 修饰可变对象即可。
  5. 修饰符有 copy ,而没有mutableCopy。 但是对象有 copy 方法和 mutableCopy 方法。copy 用于不可变对象时,只是拷贝指针而已。用于可变对象时,才会是拷贝内容。

atomic/nonatomic

  1. nonatomic 是非原子操作,虽然不那么安全,但是性能要比 atomic 高。但是当多个线程访问同一个属性,会出现无法预料的结果。
  2. atomic 生成的 setter/getter 方法会进行加锁操作。 nonatomic 生成的 setter/getter 方法不会进行加锁操作。
  3. 自动加的锁,仅仅保证了 setter/getter 存取方法的线程安全。
  4. 当几个线程同时调用同一属性的 setter/getter 方法时,会得到一个完整的值,但是呢,得到的值并不可控。
  5. atomic 并非是线程安全的,因为只是保证了 setter/getter的加锁操作。如果在进行 setter/getter的时候,有另外一个线程同时进行 release 操作,那么可能会直接 crash 。
  6. atomic 由于使用了自旋锁,保证了线程安全,但是自旋锁也比较消耗性能。

数据库操作、存储


block 的原理和写法


block 基础

先来看写法:

//无参数返回值的定义和使用
void (^noParamsBlock)(void)=^{
    NSLog(@"无入参无返回参数的block");
};
//无入参,但是有返回值的 block
int (^withBackParamsBlock)(void)=^{
    int a = 100;
    NSLog(@"有返回值:%d",a);
    return a;
};

//有入参,但是没有返回值的 block
void(^withInputParamBlock)(int x)=^(int x){
    NSLog(@"入参是:%d",x);
};

//有入参,有返回值的 block
int (^completionBlock)(int x)=^(int x){
    int b = x + 100;
    NSLog(@"最终处理的结果是:%d",b);
    return b;
};

//通过 typedef 来定义一个 block
typedef int (^oneTypedefBlock)(NSString * str);
//然后使用的地方为,注意修饰符为 copy:
@property(nonatomic,copy)oneTypedefBlock oneBlock;
//调用,先设置
    //typedef 的blokc
    self.oneBlock = ^int(NSString *str) {
        return  346;
    };
//然后调用
    int b=self.oneBlock(@"123");
    NSLog(@"b is %d",b);
  • block 的本质是C语言中的结构体
  • 外部的局部变量是直接传进 block 内,相当于 copy 了一份局部变量,在 block 代码块后面修改局部变量的值不会影响block 内的值。如下所示:
int age = 20;
void (^block)(void) =  ^{
     NSLog(@"age is %d",age);
};
age = 25;
       
block();
  • 但是如果使用 static 修饰的局部变量,那么传到 block 里的是变量的指针(其实就是这个变量的内存地址),所以在block代码块之后修改它的值,依然会影响到 block 里面的输出。
static int height  = 30;
int age = 20;
void (^block)(void) =  ^{
     NSLog(@"age is %d height = %d",age,height);
};
age = 25;
height = 35;
block();
  • 全局变量的话是直接访问的,所以不存在传递效果。 所以修改后是多少,最后就是多少
int age1 = 11;
static int height1 = 22;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^block)(void) =  ^{
            NSLog(@"age1 is %d height1 = %d",age1,height1);
        };
        age1 = 25;
        height1 = 35;
        block();

    }
    return 0;
}

block 类型

block 是有 isa 指针的结构体,它的终极父类,也是 NSObject ,所以本质上它也是一个对象。

关于 __block

虽然通过为变量增加 staic 修饰符或者改为全局变量,都可以在block内部修改变量的值,但是这样很麻烦,而且变量存在于堆内,无法快速释放。 所以最好的方法是使用 __block .
__block 的源码其实是通过找到变量的内存地址,进而进行修改的。
__block 不能修饰 static 变量或全局变量。

关于 block 的内存管理问题

__block 修饰的局部变量,会随着 block 的copy操作一同被拷贝至堆上。如果这个时候 block 内部有用这个变量,那么就会形成强引用,不会被随时释放掉。

但是如果没有经过 copy 操作,只是 __block 了一下局部变量,没有 copy block ,那么此时 block 和 局部变量都是在栈内,随时可能被释放掉。

如果多个 block 代码块都经过 copy 操作,到了堆上,共同使用一个拷贝到堆上的变量,那么这个变量只会被拷贝到堆内一次。而形成的强引用,只有在所有持有的 block 都释放掉之后,变量才会释放掉。

block 的循环引用问题

一般都是 block 内强持有了某个对象,而此对象又强持有block ,导致谁也没办法释放。
解决方案: __weak/ __unsafe_unretained
但是最好用 __weak 因为会自动指向 nil ,不会导致崩溃。

autoreleasepool 的原理


  • autorelease 是对象的引用计数自动处理器。
  • autoreleasepool的数据结构:是一个双向链表,每张链表头尾相接,有parent、child 指针。每创建一个自动释放池,就会在首部创建一个哨兵对象,作为标记。最外层池子满了,就会有个 next 指针,只想下一张链表。
  • autoreleasepool的作用:对象发送 autorelease 消息的时候,会把对象添加到 autoreleasepool 中。当自动释放池销毁的时候,自动释放池内的所有对象都会 relase。对象执行 autorelease 操作之后,该引用计数不会改变,会返回该对象。
  • autoreleasepool何时释放?1.手动释放,pool release 2. runloop循环结束,autoreleasepool内部对象,引用计数为0 ,就会被释放。
  • autoreleasepool的原理? 它是把 release 操作延迟了,把该对象放到池子中,当池子被销毁释放的时候,池子中的所有对象都会被释放。
  • 自动释放池是对池子里面所有对象发送 release 消息,不保证对象一定会被销毁,自动释放池释放之后,对象才会释放。
  • autoreleasepool进行 drain 操作时,所有标记了 autorelease 的对象的引用计数会被-1,当pool被释放时,内部所有对象都会被释放。

其他知识点

  • UIView 继承于 UIResponder ,可以响应点击事件,但是 CALayer 继承于 NSObject 则不可以响应点击事件。 UIView 是 CALayer 的管理者,真正绘图的是 CALayer 。

UIVIew 中提供了

- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;  

这两个方法来响应点击事件。其实 CALayer 的 delegate 是 UIView。
总结就是: UIView 主要是对视图的管理,而 CALayer 主要是视图的绘制。

  • UIViewController 的 viewdidLoad 和 viewWillAppear 在同一个 runloop 里
  • VC 的生命周期有:
// 类的初始化方法
+ (void)initialize;
//通过xib来初始化控制器
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil ;
// 对象初始化方法
- (instancetype)init;
// 从归档初始化
- (instancetype)initWithCoder:(NSCoder *)coder;
//归档初始化后唤醒nib
-(void)awakeFromNib;
// 加载视图:当访问UIViewController的view属性时,view如果此时是nil,那么VC会自动调用loadView方法来初始化一个UIView并赋值给view属性。此方法用在初始化关键view,需要注意的是,在view初始化之前,不能先调用view的getter方法,否则将导致死循环(除非先调用了[super loadView];)如果没有重载loadView方法,则UIViewController会从nib或StoryBoard中查找默认的loadView,默认的loadView会返回一个空白的UIView对象。
-(void)loadView;
// 视图加载完成
- (void)viewDidLoad;
// 将要展示:,在view即将添加到视图层级中(显示给用户)且任意显示动画切换之前调用,此时self.view.superview为nil.这个方法中完成任何与试图显示相关的任务,例如改变视图方向、状态栏方向、视图显示样式等。
-(void)viewWillAppear:(BOOL)animated;
// 将要布局子视图,self.view.superview为_UIParallaxDimmingView
-(void)viewWillLayoutSubviews;
// 已经布局子视图
-(void)viewDidLayoutSubviews;
// 已经展示:在view被添加到视图层级中,显示动画切换之后调用(这时view已经添加到supperView中)。在这个方法中执行视图显示相关附件任务,如果重载了这个方法,必须在方法中调用[supper viewDidAppear];,此时self.view.superview为UIViewControllerWrapperView。
-(void)viewDidAppear:(BOOL)animated;
// 将要消失:view即将从supperView中移除,移除动画切换之后调用,此时已调用removeFromSuperview。此时self.view.superview还是superview为UIViewControllerWrapperView.
-(void)viewWillDisappear:(BOOL)animated;
// 已经消失:view从superView中移除,移除动画切换之后调用,此时已调用removeFromSuperview。此时self.view.superview还是superview为nil.
-(void)viewDidDisappear:(BOOL)animated;
// 内存警告
- (void)didReceiveMemoryWarning;
// 销毁释放
-(void)dealloc;
  • UIView 的生命周期有:
//构造方法,初始化时调用,不会调用init方法
- (instancetype)initWithFrame:(CGRect)frame;
//添加子控件时调用
- (void)didAddSubview:(UIView *)subview ;
//构造方法,内部会调用initWithFrame方法
- (instancetype)init;
//xib归档初始化视图后调用,如果xib中添加了子控件会在didAddSubview方法调用后调用
- (instancetype)initWithCoder:(NSCoder *)aDecoder;
//唤醒xib,可以布局子控件
- (void)awakeFromNib;
//父视图将要更改为指定的父视图,当前视图被添加到父视图时调用
- (void)willMoveToSuperview:(UIView *)newSuperview;
//父视图已更改
- (void)didMoveToSuperview;
//其窗口对象将要更改
- (void)willMoveToWindow:(UIWindow *)newWindow;
//窗口对象已经更改
- (void)didMoveToWindow;
//布局子控件
- (void)layoutSubviews;
//绘制视图
- (void)drawRect:(CGRect)rect;
//从父控件中移除
- (void)removeFromSuperview;
//销毁
- (void)dealloc;
//将要移除子控件
- (void)willRemoveSubview:(UIView *)subview;
  • malloc 函数用于分配若干字节的内存空间,并返回头部指针,如果没有足够的空间则返回 NULL
  • 对称加密:如果加解密使用的是相同的key,那么就是对称加密,否则就是非对称加密。常用的对称加密算法:DES、3DES、Blowfish、IDEA、RC4、RC5、RC6和AES
  • 非对称加密:公钥交给对面,对方使用公钥加密后,传回的数据,只有自己的私钥能解开。常用的非对称加密算法:RSA、ECC(移动设备用)、Diffie-Hellman、El Gamal、DSA(数字签名用)
  • 为什么UI操作要在主线程?
UIKit并不是一个 线程安全 的类,UI操作涉及到渲染访问各种View对象的属性,如果异步操作下会存在读写问题,而为其加锁则会耗费大量资源并拖慢运行速度。另一方面因为整个程序的起点UIApplication是在主线程进行初始化,所有的用户事件都是在主线程上进行传递(如点击、拖动),所以view只能在主线程上才能对事件进行响应。而在渲染方面由于图像的渲染需要以60帧的刷新率在屏幕上 同时 更新,在非主线程异步化的情况下无法确定这个处理过程能够实现同步更新。
  • 扩展和类别: 扩展是在编译时就已经确定的,是可以添加变量的,随着类的存在而存在,类的消失而消失。而类别则是在运行时确定的,不可以增加变量,会破坏内存结构。
  • 不可以向在运行时创建的类中加入实例变量。因为编译好的内容已经确定了内存结构。只能添加方法、替换方法或者通过关联对象的方式来添加属性。
  • isMemberOfClass:是检测方法调用者对象的类是否等于传入的这个类。
    isKindOfClass:是判断方法调用者对象的类是否等于传入的这个类或者其子类。
  • https 加密原理: 单向认证,就是客户端从服务端获取公钥证书进行验证,成功后建立通信通道。而双向认证,客户端除了从服务端获取公钥证书以外,还需要把自己的公钥证书给服务端,两者都验证通过后才会建立通信。
  • get请求做查询。put请求做修改,后一个put请求会覆盖前一个。而post请求一般做新增,后一个不会覆盖前一个。
  • AFHttpSessionManage 内部封装了 NSUrlSession,负责网络请求的发送
  • AFRequestSeriliaztion 用来把请求参数包装成 NSUrlRequest
  • AFResponseSeriliztion 用来把 NSUrlResponse 对象解析成 json,xml
  • AFSericutyPolicy 用来处理公钥验证逻辑
  • AFNetworkReachAblityManager 监听当前网络状态的功能
  • NSOperation 支持 KVO ,可以监听任务是否完成等状态
  • runloop和线程是一一对应的关系,一个线程执行完某个任务之后,线程就会退出,想要线程不退出就需要runloop这个机制。线程有任务就唤醒线程,没有任务就让线程进入休眠状态。
  • 程序启动,默认创建主线程,并开启一个对应的 runloop。一直在处理消息和等待的循环中,直到退出。
  • 子线程的runloop是在有需要的时候才会创建,默认是没有开启runloop,runloop退出,线程就会销毁。
  • 一个对象能 copy 操作吗,可以,只要遵循 NSCopying 协议,并实现 copyWithZone: 方法。
  • 重写父类方法不能调用[super dealloc]
  • retain 引用计数+1,release 引用计数-1,当为0的时候,会调用 dealloc 来释放对象。
  • new/copy/alloc 操作会对一个对象引用计数+1
  • 类目可以为已有的类添加方法,属性,但是不能添加实例变量,也不能自动生成 setter、getter 方法,只能使用 dynamic 来手动生成,在原始类中必须存在对应的实例变量。给类目增加属性,setter 通过object_setassociateObject 来实现。getter方法通过 object_getassociated 方法来实现
  • 拓展是为已有的类增加私有的变量、方法和属性,都只能再内部实现,不允许外部调用。
  • category 实际上是 category_t 的结构体,运行的时候,方法列表是以倒叙的方式插入到原有的方法列表最前面,不同的 category 添加同一个方法,只会执行最后一个。
  • 原生和H5交互的原理:1. 原生调用H5:H5嵌入在APP中,只需要H5暴露出局部变量或者方法,APP可以通过window直接调用。2. H5调用原生:首先App想H5注入一个全局的JS对象,然后H5直接访问这个对象。然后由h5发起一个自定义协议请求,app拦截这个请求后,再由App调用H5的回调。
  • 数组元素是在内存上连续存放的,而且插入、修改需要移动大量元素,可以通过下标查找到元素,这一点比较快。
  • 链表的存储不是连续的,查找删除直接根据元素指针重新赋值即可,效率比较高。
  • 堆是内存中的二叉树:1. 它必须是个完整的二叉树。2. 它的子节点的值总是不小于或者不大于它的父节点。
  • 堆就像图书馆的书,可以随意取放,不必把书架上所有书都弄出来。
  • 栈的话,先进去的最后出来,只允许在栈顶进行操作
  • 队列的话,只能在一端进行插入操作,在另外一端进行删除操作,FIFO
  • block 本质是一个包含上下文变量的函数。
  • NSGlobalBlock: 存储在全局区,没有访问 auto 变量
  • NSStackBlock:存储在栈区,访问了 auto 变量
  • NSMallocBlock:存储在堆区,实际上是 NSStackBlock 调用了 copy

KVC 和 KVO

KVC 是键值编码

可以通过key值直接访问对象的属性,或者给对象赋值,不需要通过 setter 或者 getter 方法来操作,在运行时动态修改属性的值。
用 NSKeyValueCoding 协议,使用 keyPath 来寻找 key
kvc 底层原理: 设置值,先查找 set 方法,判断 accessInstanceVariablesDirectly 结果,yes 时,按照 _ 、_is、is来找成员变量,找到赋值,找不到调用 setValue:forUndefinedKey
取值:通过 countof,objectAtIndexs,objectsIndex 来取值

KVO 是一套监听机制

kvo 是一套监听的事件机制,通过 kvo 可以把某个属性改变的通知给监听者。主要是添加监听,实现回调,移除监听。
手动触发监听,可以使用 willChangeValueForKey、didChangeValueForKey
当一个实例的属性被观察时,这个对象的 isa 指向的是一个动态创建的中间类。
当一个对象被观察时,一个 nskvonotify_xxxx就会被动态创建,这个新类继承自被观察类(Xxxx)。
原理:在动态类中,重写被观察者属性的 setter 方法,在赋值语句前后加上响应的通知。
最后将被观察者对象的ISA指向这个动态创建的类。

method swizzing (黑魔法)

实际上实现了方法的交换。
调用方法实际上是向一个对象发送消息,查找消息的方法是 selector,利用 runtime 的特性,可以在运行的时候,替换 selector 方法的实现。
每个类都有一个方法列表,存放着方法的名字和方法的实现的映射关系,selector 是方法名,imp 是函数指针的实现。
替换方法的几种方式:
method_exchangeimplements 交换两个方法的实现
class_replacemethod 替换方法的实现
method_setimplements 直接设置某个方法的 imp

OC 的消息机制

OC 在向一个对象发送消息时,发生了什么?
在向一个对象发送消息的时候,runtime 会根据对象所指向的 isa,找到该对象实际所属于的类,然后在该类的方法列表及其父类的方法列表中寻找方法并执行,如果一直到根类还没找到,转向拦截调用,走消息转发机制,一旦找到它,就去执行它的 IMP。

weak 置空原理

创建存储表,dealloc 置空的时候,表清空

weak表是一张哈希表,key存储该对象的指针,value 存储该对象weak指针的地址数组。

  1. 初始化时,会调用 objc_initWeak函数,初始化新的 weak 指针指向对象的地址。
  2. 调用 objc_sotreWeak 函数,更新指针的指向,创建弱引用表。
  3. 最后调用 clearDeallocating 函数,根据对象的地址找到 weak 指针的数组,遍历这个数组,其中对象置为nil,然后删除这个 entry ,清理记录

Hash 是什么,原理是什么

hash 是根据关键字key直接访问在内存的存储位置的数据结构,本质上是一个数组,数组中每一个元素是一个箱子,每个箱子存放的是键值对,根据 index 取出 value,
index的生成是通过哈希函数,将key转换成 index。无论哈希函数式怎样的,都可能会出现 index 冲突。

  1. hash 是一种高效查找的数据结构,时间复杂度为O(1)
  2. hash 表实际上是一个数组,存储的时候,根据关键字 key,通过散列函数生成一个哈希地址,存储到数组中,若地址是一样的,可以以链地址的方法存储起来。
  3. 优点:哈希表可以提供快速的操作
  4. 缺点:哈希表通常是基于数组的,数组创建之后难以维护。
  5. 解决冲突的方案:链地址法(拉链法):数组+链表

数据持久化方案

  • userDefault:用来存储用户信息、用户偏好设置,位置在沙盒的 preference 文件夹里的 plist 文件中。只能写入不可变的数据(nsnumber/nsstring/nsdate/nsarray/nsdictionary/bool),不能存储自定义对象。写入磁盘时,不是立即写入,而是使用 synchronize 才会立即写入。
  • keychain:存储密码等重要数据,app删除时,这些数据不会删除,KeychainItemWrapper 三方库可以使用keychain安全的存储容器,存在系统中的,并不是沙盒里,App删除后,也存在手机中。使用官方的 KeychainItemWrapper,第三方封装的 sskeychain,通过 Security.framework 框架使用。
  • plist 和 userdefault 差不多。
  • 归档:直接把对象存储成文件,把文件读成对象,对象归档的文件是保密的,村收 NSCoding 协议。实现协议的方法:encodeWithCode,initWithCoder。
  • 沙盒:在Documents下面,存储非机密的数据,temp 、library/caches 保存应用运行时生成的需要持久化的数据。而 library/preference 保存应用的所有偏好设置。
  • 数据库 : sqlite、FMDB 数据库对象。
  • layoutIfNeed 一旦被调用,会立刻在刷线程刷新所有View。而 layoutSubView 只有系统能调用,我们可以重写此方法,让系统按照我们预想的来绘制View。 setNeedLayout 并不会直接强制视图重新布局,而是在下个周期才重新布局。
  • 离屏渲染是指GPU在当前屏幕缓冲区之外新开辟一个缓冲区进行渲染操作。离屏渲染耗时,主要是因为创建缓冲区和上下文切换,创建新的缓冲区代价不大,但是上下文切换代价很大。
  • 离屏渲染的操作:1. 为图层设置遮罩层 2. 将图层的 layer.masksToBounds 设置为 true 3.将图层的透明度设置为小于1.0 4. 为图层设置阴影 5. 基本上都是图层的操作
  • 常用的动画:UIView animation UIViewPropertyAnimator 、CALayer Animation
  • loadView 用来自定义 view,只要实现了这个方法,其他通过 xib 或者 storyboard 创建的 view 都不会被加载
  • UITableViewCell的复用机制:每次通过标识符创建 cell 的时候,先去缓存池中找指定标识的 cell,如果没有就直接返回 nil。此时会调用 initWithStyle:reuseIdentifier:来创建一个 cell 。 当 cell 离开界面就会被放到缓存池中,以供下次复用。
  • imageNamed 会自动魂村新加载的图片,并且重复利用缓存图片,一般用于App内经常用的尺寸不大的图片。
  • imageWithContentsOfFile 根据路径加载没有缓存和有缓存的过程,用于一些大图,使用完毕会释放内存。
  • 类和结构体的区别:
    类是引用类型,结构体是复制类型。
    类可以封装属性、封装方法、可以继承,结构体只能封装属性。
    类对象存储在堆,访问速度较慢,效率低。结构体在栈区,访问速度快。
    结构体赋值是对属性的赋值,而类赋值是对象应用增加。
    类能够在运行时检查和结石类实例的类型。

网络编程 socket

套接字:每个套接字由一个IP+端口号组成。 支持tcp/ip协议的网络通信,包含5种信息:连接使用的协议、本地主机的ip、本地进程的协议端口号、远程主机、远程进程的协议端口。
连接过程分为三步:服务器监听、客户端请求、连接确认。
服务器监听:服务器处于等待的状态,实时监控网络状态,等待客户端的连接请求。
客户端请求:客户端发送请求时,会带着自己的套接字指出所要连接的服务端的套接字的地址和借口,然后向服务端套接字发送连接请求。
请求确认:当服务端监听到或者受到客户端套接字请求的时候,就响应客户端请求,简历一个新的线程,把服务端的套接字发送给客户端,一旦客户端确认就简历连接请求。而服务端套接字继续处于监听状态,等待其他客户端套接字的连接请求。

创建Socket连接时,可以指定使用的传输层协议,Socket可以支持不同的传输层协议,当使用TCP协议进行连接时,该Socket连接就是一个TCP连接。

由于通常情况下Socket连接就是TCP连接,因此Socket连接一旦建立,通信双方即可开始相互发送数据内容,直到双方连接断开。但在实际网络应用 中,客户端到服务器之间的通信往往需要穿越多个中间节点,例如路由器、网关、防火墙等,大部分防火墙默认会关闭长时间处于非活跃状态的连接而导致
Socket 连接断连,因此需要通过轮询告诉网络,该连接处于活跃状态。
而HTTP连接使用的是“请求—响应”的方式,不仅在请求时需要先建立连接,而且需要客户端向服务器发出请求后,服务器端才能回复数据。

App 启动优化策略,启动优化操作

premain阶段

premain 阶段为 main 函数执行之前所做的操作

  1. 加载 mach-o 文件(包含了所有的可执行代码)
  2. 加载动态链接库 dyld (dynamic loader)
  3. 定位内部、外部指针引用,例如字符串方法
  4. 加载类扩展中的方法
  5. C++ 静态对象加载,调用 objc 的 + load 函数
  6. 执行声明为 attribute 的c函数

其实也就是如下工作:
加载mach-o文件,并获取 dyld 的路径。
dyld初始化运行环境,并开启dyld缓存策略,从可执行文件依赖的顺序开始,递归加载所有动态链接库,链接库会通过 dyld 把mach-o文件实例化为image镜像文件
当dyld对所有的依赖库初始化后,此时runtime会对项目中所有类进行类结构初始化,然后调用所有类的 load() 方法
dyld最后会返回 main()函数地址
main()函数被调用,然后进入 AppDelegate

load ,在main函数调用之前,类被注册加载到内存中,load方法会被调用,也就是说每个类的load方法都会被调用一次。
在该方法中,我们最常用到的场景,就是使用 runtime 提供的交换函数:
OBJC_EXPORT void method_exchangeImplementations 去改变系统方法行为,并添加自定义行为。

优化点:

  1. 减少无用的类方法,减少 +load 操作,减少 attribute 的 c 函数,减少启动加载的dy。
  2. 删减一些无用的静态变量。
  3. 删减没有被调用或者已经废弃的方法
  4. 合并功能类似的类或者扩展
  5. 压缩图片资源

main 阶段

main 函数到首页展示的阶段。

  1. 调用 main()函数
  2. 调用 UIApplicationMain()
  3. 调用 applicationWillFinishLaunching

优化点:

  1. 减少启动时的非必要操作,延迟到首页展示后加载,统计并优化耗时的方法、对于一些放在子线程的操作尽量不要放在主线程。
  2. 能用懒加载用懒加载,能延迟初始化就延迟,不能卡主线程创建的时间。
  3. instruments 的 timeProfile 来统计耗时的方法,或者通过打印时间的方式,对业务逻辑进行优化。
  4. instruments 统计启动时所加载的 + load 方法以及耗费的时间,对不必要的 +load 进行优化
  5. 尽量使用纯代码而不是 xib或者 storyboard来进行UI框架的搭建,多了一层渲染的时间。
  6. 减少不必要的库,压缩图片,合并类似的类和扩展

Crash 防止及调试

  1. 数组越界,插入nil等,需要多做判断来预防
  2. kvo的crash、dealloc时,对象没有移除通知,或者重复添加和移除该通知。可以通过 kvoInfomaps 来管理 keypath 所对应的 infoarray,重写 addserver、removeObserve、observerValueForKey
  3. 如果是找不到方法引起的错误 unrecognized_selector_metod_crash ,那么我们需要动态创建一个桩类,为该类添加对应的 selector ,用一个返回0 的函数来实现 sel 中的 imp。将错误消息转发到这个桩类的对象上,从而使得App安全运行。

导致APP卡顿的原因

  1. 死锁:主线程和子线程互相等待对方释放锁。
  2. 抢锁:主线程需要读取DB,此时子线程正在往DB里面插入数据,就会造成卡顿一会,就汇付。
  3. 主线程大量的IO操作:主线程写入大量的数据,导致卡段。
  4. 主线程大量的计算:程序算法不合理,大量的 for 循环,导致主线程某个函数大量占用CPU
  5. 主线程大量的绘制:复杂的UI,大量的图文混排等,我们可以使用 AsnycDisplayKit 框架进行预排版,异步绘制,图片解码等。
  6. 判断主线程卡顿:FPS降低,runloop运行时间长,CPU使用率很高。

UItableView 优化

  1. 复用 cell
  2. 数据放到子线程中处理
  3. 异步加载图片
  4. 图片要有缓存机制
  5. SDWebimage,在子线程处理,然后返回主线程操作
  6. 图片的圆角操作,可以设置 layer 的 shouldRasterize 属性为 YES,可以将负载转移给CPU
  7. 滑动过程中不加载图片,等待滑动结束加载图片
  8. tableView使用懒加载

SDWebimage 的大致原理

通过为 UIImageView添加类别,来拓展它的功能。

  1. 根据URL从内存中读取缓存
  2. 若内存中没缓存,则从本地读取
  3. 若本地没有,则根据URL从网络下载,下载完成之后进行解码。

UIWindow

UIWindow 继承自 UIView,作为根视图来装 View 元素。UIWindow 提供了一个区域用于显示 UIView,并将事件分发给 UIView ,一般一个应用只有一个 UIWindow。

原生和RN的交互原理

原生调用RN

  1. 原生需要创建一个继承于RCTEventEmiter的类,实现RCTBridgeModule协议,rct_export_module把该类暴露出去。原生需要实现 supportedEvents 方法,把需要的事件名字放进去。
  2. RN 端需要创建 NativeEventEmiter,来订阅支持的事件 name,addlistener
  3. 原生通过通知的方式把这个事件发送出去,RN那边来监听。

RN调用原生方法

  1. 原生创建一个类,遵循 RCTBridgeModule 协议,在类中把需要给RN调用的方法,使用 rct_export_method 暴露出去。
  2. 若是需要原生回调信息给RN,可以通过 RCTPromiseResolveBlock、RCTPromiseRejectBlock 进行信息回调。
  3. RN 调用通过 NativeModules.类名.方法名

组件化