练习 05 内存/pool/arc/循环引用/通知机制(多代理)

318 阅读24分钟

OC语言的特性,


1. 内存管理的理解

Objective-C内存管理是基于 引用计数的。
MRC就是调用Objective-C的方法(alloc/new/copy/mutableCopy/retain/release等)实现引用计数的增加和减少
ARC 时代后,针对返回的对象编译器也做了一些特殊处理,
autorelease就是自动释放池

MRC
时代有一句话叫 谁创建谁释放 ,意思是由开发者通过allocnewcopymutableCopy等方法创建的对象,需要开发者手动释放,而由其他方法创建并返回的对象返回给用户后也不需要开发者释放,比如说由[NSMutableArray array]方法创建的数组,这样的对象默认由自动释放池管理。


ARC 时代后,针对返回的对象编译器也做了一些特殊处理,
[NSMutableArray array] 的优化
相对于用户创建的对象,[NSMutableArray array]方法创建返回的对象转换后多出了一个objc_retainAutoreleasedReturnValue方法。
这涉及到一个最优化处理:
为了节省了一个将对象注册到autoreleasePool的操作,在执行objc_autoreleaseReturnValue时,根据查看后续调用的方法列表是否包含objc_retainAutoreleasedReturnValue方法,以此判断是否走优化流程。 在执行objc_autoreleaseReturnValue时,优化流程将一个标志位存储在 TLS (Thread Local Storage) 中后直接返回对象。
执行后续方法objc_retainAutoreleasedReturnValue时检查 TLS 的标志位判断是否处于优化流程,如果处于优化流程中则直接返回对象,并且将 TLS 的状态还原。


weak实现
从 SideTables 中取出存储弱引用表的 SideTable(为弱引用表 weak_table_t 的一层封装)

  1. 从全局的哈希表SideTables中,利用对象本身地址进行位运算后得到对应下标,取得该对象的弱引用表。SideTables是一个 64 个元素长度的散列表,发生碰撞时,可能一个SideTable中存在多个对象共享一个弱引用表。
  2. 如果有分配新值,则检查新值对应的类是否初始化过,如果没有,则就地初始化。
  3. 如果 location 有指向其他旧值,则将旧值对应的弱引用表进行注销。
  4. 如果分配了新值,将新值注册到对应的弱引用表中。将isa.weakly_referenced设置为true,表示该对象是有弱引用变量,释放时要去清空弱引用表。


  1. 通过弱引用指向的对象,获取弱引用表,并且将其上锁,防止在此期间被清除。
  2. 判断是否包含自定义retain方法,如果没有,则使用默认rootTryRetain方法,使引用计数 + 1 。
  3. 如果使用了自定义retain方法,则调用自定义方法,在调用之前会先判断该对象所属类是否已经初始化过,如果没有初始化会先进行初始化然后再调用。

2. aotoreleasepool

自动释放的底层:
在 ARC 环境下, __autorelease 修饰符可以将对象加入自动释放池中,由自动释放池管理释放。

  • @autoreleasepool{}关键字通过编译器转换成objc_autoreleasePoolPushobjc_autoreleasePoolPop这一对方法。

  • 自动释放池作用域里的3个函数
  • objc_autoreleasePoolPush作为自动释放池作用域的第一个函数。
  • 使用objc_autorelease将对象加入自动释放池。
  • objc_autoreleasePoolPop作为自动释放池作用域的最后一个函数。

自动释放池都是由一个或者多个AutoreleasePoolPage组成,page的 SIZE 为 4096 bytes ,它们通过parentchild指针组成一个双向链表。

自动释放的释放时机
其实本质上说是在   本次runloop迭代结束时清理掉  被本次迭代期间被放到autorelease pool中的对象的。


3、哪些情况使用autoreleasepool

//// 短时间创建大量临时变量,函数执行完后 他的作用域 就会销毁掉。为什么还要在这里加呢

  • 写给予命令行的程序时,就是没有UI框架;
  • 写循环,循环里边包含了大量临时创建的对象;
  • 创建了新的线程;
  • 长时间在后台运行的任务;

合理运用自动释放池,可以降低程序的内存峰值,异步的方式将文件保存在磁盘

4. ARC的底层实现原理。系统接管了,我们自己实现对引用技术 C++里的


ARC 是 iOS 中管理引用计数的技术,帮助 iOS 实现垃圾自动回收,具体实现的原理是由编译器进行管理的,同时运行时库协助编译器辅助完成。主要涉及到 Clang (LLVM 编译器) 和 objc4 运行时库。

在分析 ARC 相关源码之前,需要对 isa 有一定了解,其中存储了一些非常重要的信息,下面是 isa 的结构组成:

工会/联盟union isa_t 
{
    Class cls;
    uintptr_t bits;
    struct {
         uintptr_t nonpointer        : 1;//->表示使用优化的isa指针
         uintptr_t has_assoc         : 1;//->是否包含关联对象
         uintptr_t has_cxx_dtor      : 1;//->是否设置了析构函数,如果没有,释放对象更快
         uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000 
                                               ->类的指针
         uintptr_t magic             : 6;//->固定值,用于判断是否完成初始化
         uintptr_t weakly_referenced : 1;//-> 对象是否被弱引用
         uintptr_t deallocating      : 1;//-> 对象是否正在销毁
         uintptr_t has_sidetable_rc  : 1;//1->在extra_rc存储引用计数将要溢出的时候,
                                         借助Sidetable(散列表)存储引用计数,
                                         has_sidetable_rc设置成1
        uintptr_t extra_rc          : 19;  //->存储引用计数
    };
};

其中nonpointerweakly_referencedhas_sidetable_rcextra_rc都是 ARC 有直接关系的成员变量,其他的大多也有涉及到。

objc_object就是 isa 基础上一层封装,objc_class继承了objc_object,结构如下:
  • isa:objc_object 指向类,objc_class 指向元类。
  • superclass:指向父类。
  • cache:存储用户消息转发优化的方法缓存和 vtable 。
  • bits:class_rw_t 和 class_ro_t ,保存了方法、协议、属性等列表和一些标志位。



objc_retain

代码分成 3 个小分支:

  • TaggedPointer:值存在指针内,直接返回。
  • !newisa.nonpointer:未优化的 isa ,使用sidetable_retain()
  • newisa.nonpointer:已优化的 isa , 这其中又分 extra_rc 溢出和未溢出的两种情况。
    • 未溢出时,isa.extra_rc + 1 完事。
    • 溢出时,将 isa.extra_rc 中一半值转移至sidetable中,然后将isa.has_sidetable_rc设置为true,表示使用了sidetable来计算引用次数。

objc_release

将其分解后和 rootRetain 逻辑类似:

  • TaggedPointer: 直接返回 false。
  • !nonpointer: 未优化的 isa 执行 sidetable_release。
  • nonpointer:已优化的 isa ,分下溢和未下溢两种情况。
    • 未下溢: extra_rc--。
    • 下溢:从 sidetable 中借位给 extra_rc 达到半满,如果无法借位则说明引用计数归零需要进行释放。其中借位时可能保存失败会不断重试。

到这里可以知道 引用计数分别保存在isa.extra_rcsidetable中,当isa.extra_rc溢出时,将一半计数转移至sidetable中,而当其下溢时,又会将计数转回。当二者都为空时,会执行释放流程

rootRetainCount

objc_object::rootRetainCount方法是用来计算引用计数的。通过前面rootRetainrootRelease的源码分析可以看出引用计数会分别存在isa.extra_rcsidetable中。




创建hashmap表, 是全局的表嘛,

全局的话 会有线程安全问题吗、 锁的力度问题  都加锁效率会非常低的。
锁的力度拆分、我们操作的并不是整个数据结构,而是key 映射的数据结构 引用计数,
应用的那个应用列表 比如小表上拆分,key值 应用
存储是hashtable key存储的地址   value 可能是map ,只在map上加锁



5. 如何处理循环引用,

A- > timer  -> self A
1.  选择合适的时机手动释放timer(该方法并不太合理)
        (
viewDidDisappear、removeFromSuperview)时机不一定满足

2.  timer使用block方式添加Target-Action

这里我们需要自己在NSTimer的分类中添加类方法:

@implementation NSTimer (BlcokTimer)

+ (NSTimer *)bl_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void (^)(void))block repeats:(BOOL)repeats {
    
    return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(bl_blockSelector:) userInfo:[block copy] repeats:repeats];
}

+ (void)bl_blockSelector:(NSTimer *)timer {
    
    void(^block)(void) = timer.userInfo;
    if (block) {
        block();
    }
}

__weak typeof(self) weakSelf = self;
self.timer = [NSTimer bl_scheduledTimerWithTimeInterval:1 block:^{
     [weakSelf changeText];
} repeats:YES];
过block的方式 获取action,实际的target设置为self,即NSTimer类。
这样我们在使用timer时,由于target的改变,就不再有循环引用了。
使用中还需要注意block可能引起的循环引用,所以使用weakSelf




3、给self添加中间件proxy 代理,  通过利用 消息转发实现 (NSProxy + 消息转发)

需要打破这些相互引用关系,因此添加一个中间件,弱引用self,同时timer引用了中间件,这样通过弱引用来解决了相互引用,如图:


@interface PFProxy : NSProxy

//通过创建对象
- (instancetype)initWithObjc:(id)object;

//通过类方法创建创建
+ (instancetype)proxyWithObjc:(id)object;

@end
@interface ZYWeakObject()

@property (weak, nonatomic) id weakObject;

@end

@implementation ZYWeakObject

- (instancetype)initWithWeakObject:(id)obj {
    _weakObject = obj;
    return self;
}

+ (instancetype)proxyWithWeakObject:(id)obj {
    return [[ZYWeakObject alloc] initWithWeakObject:obj];
}

仅仅添加了weak类型的属性还不够,为了保证中间件能够响应外部self的事件,需要通过消息转发机制,让实际的响应target还是外部self,这一步至关重要,主要涉及到runtime的消息机制。

/**
 * 消息转发,让_weakObject响应事件
 */
- (id)forwardingTargetForSelector:(SEL)aSelector {
    return _weakObject;
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    void *null = NULL;
    [invocation setReturnValue:&null];
}

- (BOOL)respondsToSelector:(SEL)aSelector {
    return [_weakObject respondsToSelector:aSelector];
}

接下来就可以这样使用中间件了: 

// target要设置成weakObj,实际响应事件的是self
ZYWeakObject *weakObj = [ZYWeakObject proxyWithWeakObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakObj 
                 selector:@selector(changeText) userInfo:nil repeats:YES];

A 强持有 timer ,timer的target是 C, C消息转发给 弱引用对象 A

这个C对象 ,是通过NSProxy这个伪基类(相当于ViewController1的复制类),避免直接让timer和viewController造成循环。



4. 在iOS 10.0以后,苹果官方新增了关于NSTimer的三个API:三种 Block的回调方法

timerWithTimeInterval:repeats:block:
scheduledTimerWithTimeInterval:repeats:block:
initWithFireDate:repeats:block:

使用很简单,但是要注意两点:

1. 避免block的循环引用,使用__weak和__strong来避免
2. 在持用NSTimer对象的类的方法中-(void)dealloc调用NSTimer 的- (void)invalidate方法


6. 自己实现一种通知机制。observer 

通知的理解

在多线程应用中,Notification在哪个线程中post,就在哪个线程中被转发,而不一定是在注册观察者的那个线程中。

注册和监听 可能不在一个线程

那么怎么才能做到一个Notification的post线程 与  转发线程不是同一个线程呢?

这里讲到了“重定向”,就是我们在Notification所在的默认线程中捕获这些分发的通知,然后将其重定向到指定的线程中。

方式一:利用提供的block  函数

从 iOS4 之后苹果提供了带有 block 的 NSNotification。使用方式如下:

addObserverForName: object: queue:

方式二:自定义通知队列

自定义一个通知队列(注意,不是NSNotificationQueue对象,而是一个数组),让这个队列去维护那些我们需要重定向的Notification。
我们仍然是像平常一样去注册一个通知的观察者,当Notification来了时,先看看post这个Notification的线程是不是我们所期望的线程,如果不是,则将这个Notification存储到我们的队列中,并发送一个信号(signal)到期望的线程中,来告诉这个线程需要处理一个Notification。指定的线程在收到信号后,将Notification从队列中移除,并进行处理。

这种实现在几个方面受到限制。

首先,该对象处理的所有线程通知必须通过相同的方法(processNotification:)。
其次,每个对象必须提供自己的实现和通信端口。

更好的实现方式是我们去子例化一个NSNotificationCenter,然后自定义相关的处理。该类为每个线程提供一个通知队列,并且能够向多个观察者对象和方法交付通知。



方式三:多代理模式的设计和实现

使用代理可以通过中间层降低数据层和视图的耦合度,代理可以做到实时通知。

OC的常用的消息传递方式有很多种,代理、通知、block、kvo等,
不同的实现方式有不同的应用场景,也有各自的优缺点。普通的代理模式只能应用与1对1的场景,针对1对多的场景只能被迫选择使用通知。

但是通知也有自己的缺点:

  1. 在编译期间不会检查通知是否能够被观察者正确的处理;

  2. 在释放通知的观察者时,需要在通知中心移除观察者;(iOS9以前)

  3. 在调试的时候,通知传递的过程很难控制和跟踪;

  4. 发送通知和接收通知时需要提前知道通知名称,如果通知名称不一致,会出现不同步的情况;

  5. 通知发出后,不能从观察者获得任何的反馈信息。对于需要返回值的场景没有办法处理。

如果代理模式能支持多个响应对象,那么就不会再有以上的问题。

多代理最初的构思:
如果现在将delegate变为id <ReportDelegate> delegate的数组delegates。在sendOrder方法中遍历delegates数组去调用每个delegate执行代理方法不就好了。

每个方法都要主动遍历调用每个代理对象。我们自己新建的类还好,如果我们需要将第三方库的类变为多代理,想想那么多的代理方法需要改动。倘若第三方库的类新增了部分代理方法,我们也要相应的添加。

如果不想修改第三方库的代码,怎么办,难道要在外面再封装一层吗?想想以后的维护工作就让人头疼。




示例:
电商类应用中,购物车的本地数据结构是相对比较复杂的,并且购物车是一个共享的组件,在其他页面的数据更新比较难及时同步到所有引用它的 instance页面,如果不注意设计,这个共享的组件很容易造成非常高的耦合度

购物车数据结构这个组件叫做 DB

需要接收数据更新的视图组件以 view_‘count’的形式命名,如view_1
多代理意味着 DB需要持有 view_1..view_n的 instance,在数据更新的时候依次把结果通知到代理方法,我们需要一个数组存储这些 instance。

注意 多代理的 instance需要被 weak reference,否则 view的释放会造成 DB持有野指针

为了解决数组的 weak reference,我选用了 NSPointerArray
这表明 NSPointerArray是可以跟踪集合中的对象内存的,并且它是 mutable的。


为了耦合度更低,这个方法更通用,我编写了一个独立的类来表示这种多代理模式

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NVMMultiDelegate : NSObject

@property (nonatomic, readonly) NSPointerArray *delegates;

- (void)addDelegate:(id)delegate;
- (void)removeDelegate:(id)delegate;

@end

NS_ASSUME_NONNULL_END

对外仅暴露 readonly的 delegates,和增删 delegate的方法。

- (instancetype)init {
  if (self = [super init]) {
    _delegates = [NSPointerArray weakObjectsPointerArray];
  }
  return self;
}

在初始化 delegates的 Ivars的时候,指定其按照弱引用的内存管理方式来跟踪集合内的指针

添加对象指针

- (void)addDelegate:(id)delegate {
  [_delegates addPointer:(__bridge void*)delegate];
}  

NSArray在概念上不一样的地方是,NSPointerArray需要添加的是对象的指针地址,尽管他俩都是在操作指针。所以在添加对象时,需要将其转换为指针类型,__bridge转换十分具有 CoreFoundation特色


移除对象指针

- (void)removeDelegate:(id)delegate {
  NSUInteger index = [self indexOfDelegate:delegate];
  if (index != NSNotFound) {
    [_delegates removePointerAtIndex:index];
  }
  [_delegates compact];
}

- (NSUInteger)indexOfDelegate:(id)delegate {
  for (NSUInteger i = 0; i < _delegates.count; i += 1) {
    if ([_delegates pointerAtIndex:i] == (__bridge void*)delegate) {
      return i;
    }
  }
  return NSNotFound;
}
  

移除就稍显麻烦了,因为 NSPointerArray并不像 NSArray那样具有非常便捷的 API,这也是它自己的功用造成的结果。所以我们需要自己写一下 indexOf API,考虑到多代理模式的对象并不会非常多,我们就用普通的快速遍历好了。


核心:事件转发

在 Objc的响应链中,判断一个对象是否可以执行 Selector可以使用 respondsToSelector方法,首先我们需要复写这个方法,1. 让调用者知道我们的多代理对象可以响应它存的delegates数组里对象的方法

- (BOOL)respondsToSelector:(SEL)aSelector {
  if ([super respondsToSelector:aSelector]) {
    return YES;
  }
  for (id delegate in _delegates) {
    if (delegate && [delegate respondsToSelector:aSelector]) {
      return YES;
    }
  }
  return NO;
}  


调用者在得知可以响应开始调用后,Objc在 Runtime时期会去 2. 取方法签名,通过复写这个方法,我们替换掉默认的方法签名,让调用者可以获得他想要在 delegates对象数组里的方法签名

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
  NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
  if (signature) {
    return signature;
  }
  [_delegates compact]; //***这个方法可以帮助你去掉数组里面的野指针,
                          //避免你在快速遍历的时候拿到一个指向不存在对象的地址
  for (id delegate in _delegates) {
    if (!delegate) {
      continue;
    }
    signature = [delegate methodSignatureForSelector:aSelector];
    if (signature) {
      break;
    }
  }
  return signature;
}

在拿到方法签名后,接下来会去调用,同样地,我们需要复写这个方法

- (void)forwardInvocation:(NSInvocation *)anInvocation {
  SEL selector = [anInvocation selector];
  BOOL responded = NO;
  for (id delegate in _delegates) {
    if (delegate && [delegate respondsToSelector:selector]) {
      [anInvocation invokeWithTarget:delegate];
      responded = YES;
    }
  }
  if (!responded) {
    [self doesNotRecognizeSelector:selector];
  }
}


这样可以保证添加在 delegates数组里面所有能响应这次调用得代理对象都获得调用,如果没有任何对象能响应这次调用,那肯定是会造成一次未识别 selector的调用,这个调用一般会产生 exception,造成应用闪退,如果你不想在这个未得到灰度验证的类中发生 crash,最好能覆盖一下 doesNotRecognizeSelector方法


使用

在使用这个类时,只需要声明一个 property:@property (nonatomic, strong) id multidelegate

声明其为 id类型只是希望在 build time能通过方法检查,因为多代理方法并没有显式声明在这个类上


比如你可以把 tableview的 delegate和 datasouce 通过[multidelegate addDelegate:tableViewDelegate]的方式添加到 multidelegate中,然后指定

tableview.delegate = multidelegate; 
tableview.dataSource = multidelegate

就可以了,当然这只是演示其用法,最好的方法是 DB设置多个页面到 multidelegate,DB在更新数据时只需调用一次 [multidelegate didUpdateData:data]这种模式就可以了


注意, NSPointerArray因为有跟踪内存的作用,所以它的性能并不是非常好,并不推荐把应用程序声明周期里的代理添加到同一个 multidelegate中,这数量可能到达上千个,正好是性能瓶颈体现比较明显的数量




如果放到数组里会有个引用,理论上 怎么弱持有对象的

选择NSPointerArray,是因为NSPointerArray不增加成员的引用计数,相当于弱引用,在释放一个delegate前,就算不将其从delegates数组中移除也不会有问题。可以持有nil,可以跟踪集合中的对象内存的,并且它是 mutable的。



7. 通知 发出到接收 是同步的还是异步的,会不会有阻塞。

如果是同步,大量的通知的话,会阻塞。

iOS里的事件广播机制是同步的,默认情况下,广播一个通知,会阻塞后面的代码:

同一个线程中(广播事件的线程),广播事件之后的代码被阻塞,直到所有的侦听者都执行完响应。

所以,由于NotificationCenter的这个特性,
如果希望广播的事件异步处理,则需要在  侦听者的方法里 开启新线程。应该把Notification作为组件间解耦的方式,而不是利用它来实现异步处理。


8. TCP连接在哪一层开发。第三方库基础上


9.面向连接怎么理解

面向连接  这个是虚拟连接,是相对物理连接的,通过双向的消息、消息确认来模拟物理连接。

TCP就是面向连接的、消息收发 并且有消息确认的过程
UDP则不是,他只管发,不管接收情况


10. 为什么三次握手,四次挥手 挥手为什么需要四次?

建立一个连接需要三次握手,而终止一个连接要经过四次挥手(也有将四次挥手叫做四次握手的)。
这由TCP的半关闭(half-close)造成的。所谓的半关闭,其实就是TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。


因为当    服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。
其中ACK报文是用来应答的,SYN报文是用来同步的

但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,"你发的FIN报文我收到了"。

只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四次挥手。

2MSL等待状态

为了保证客户端发送的最后一个ACK报文段能够到达服务器。
因为这个ACK有可能丢失,从而导致处在LAST-ACK状态的服务器收不到对FIN-ACK的确认报文。
服务器会超时重传这个FIN-ACK,接着客户端再重传一次确认,重新启动时间等待计时器。
最后客户端和服务器都能正常的关闭。
假设客户端不等待2MSL,而是在发送完ACK之后直接释放关闭,一但这个ACK丢失的话,服务器就无法正常的进入关闭连接状态。




11. 两个App通信,是通过网络层传输是怎么封装的 网络层  怎么通讯的。

通过TCP进行传输都必须先转化成二进制数据.
另外, TCP实现出于传输效率考虑,
往往会在连接两端各自开辟一个 发送数据缓冲区 和一个 接收数据缓冲区.
因此, 有时应用层通过Socket向连接中写入数据时, 数据其实并没有立即被发送, 而是被放入缓冲区等待合适的时机才会真正的发送.


主要是  应用层报文  和  TCP报文
应用层报文小于1MSS , 

但实际情况中, 因为Nagle算法/网络拥堵/拥塞控制/接收方读取太慢等等各种原因, 数据很有可能会在发送缓冲区/接收缓冲区被累积. 所以, 上面的流程更可能是这样:

发送端积压


或者这样:  接收端累积


上面的图都假设应用层报文不到一个MSS(一个MSS一般为1460字节, 这对大部分非文件请求来说都足够了), 当报文超过一个MSS时, TCP底层实现会对报文进行拆分后多次传输, 这会稍微复杂些(不想画图了), 但最后导致的问题是一致的, 解决方案也是一致的.


粘包问题:
得益于TCP协议是可靠的传输协议
(可靠意味着TCP实现会保证数据不会丢包, 也不会乱序), 粘包的问题很好处理.
我们只需要在发送方给每段数据都附上一份描述信息(描述信息主要包括数据的长度, 解析格式等等), 接收方就可以根据描述信息从一串数据流中分割出单独的每段应用层报文了.

定义报文协议:
自定义通讯协议时, 往往和项目业务直接挂钩, 所以这块其实没什么好写的. 


实现通讯协议

有了协议以后, 就可以写代码进行实现了. 

现在我们已经有了TCP连接, Request, Response和Task, 接下来要做的就是把这一切串起来. 具体来说, 我们需要一个管理方建立并管理TCP连接, 提供接口让调用方通过Request向连接中写入数据, 监听连接中读取到的粘包数据并将数据拆分成单个Response返回给调用方.

具体实现参考:
www.jianshu.com/p/2f9882373…


心跳

Ping-Pong通常用来验证TCP连接的有效性. 具体来说, 如果Ping-Pong正常, 那么证明连接有效, 数据传输没有问题, 反之, 要么连接已断开, 要么连接还在但服务器已经过载无力进行恢复, 此时客户端可以选择断开重连或者切换服务器.



文件下载/上传?

到目前为止, 我们讨论的都是类似DataTask的数据请求, 并未涉及到文件下载/上传请求, 事实上, 我也没打算在通讯协议上加上这两种请求的支持. 这部分我是这样考虑的:

如果传输的文件比较小, 那么仿照HTTP直接给协议加上ContentType字段, Content以特殊分隔符进行分隔即可.

如果传输的文件比较大, 那么直接在当前连接进行文件传输可能会阻塞其他的数据传输, 这是我们不希望看到的, 所以一定是另起一条连接专用于大文件传输. 考虑到文件传输不太可能像普通数据传输那样需要即时性和服务端推送, 为了节省服务端开销, 文件传输完成后连接也没有必要继续保持. 这里的"建立连接-文件传输-断开连接"其实已经由HTTP实现得很好了, 而且功能还多, 我们没必要再做重复工作.

基于以上考虑, 文件传输这块我更趋向于直接使用HTTP而不是自行实现.


WebSocket

就我自己而言, 使用TCP只是看重TCP的全双工通信和即时性而已, 虽然TCP Socket已经大大降低了TCP的使用门槛, 但门槛依然存在, 使用者仍不可避免的需要对TCP有个大体了解, 还需要处理诸如"粘包""心跳"之类的细节问题. 如果你的需求只是需要全双工通信和即时性的数据传输, 并且对灵活性和流量要求不敏感的话, 那么我更推荐你使用近乎零门槛的WebSocket.

从名字和接口来看, WebSocket有点像TCP Socket, 但它并不属于Socket. WebSocket和HTTP一样, 是基于TCP的应用层协议, 它在保留了TCP的全双工通信的同时还提供了以应用层报文为传输单位和Ping-Pong的功能. 对我们来说, WebSocket用起来就像自带"粘包处理"和"心跳"功能的TCPSocket, 非常方便.




12. NSProxy

翻译:NSProxy是一个抽象的超类,它定义了一个对象的API,用来充当其他对象或者一些不存在的对象的替身。
通常,发送给Proxy的消息会被转发给实际对象,或使Proxy加载(转化为)实际对象。
 NSProxy的子类可以用于实现透明的分布式消息传递(例如,NSDistantObject),或者用于创建开销较大的对象的惰性实例化。


NSObject类是Objective-C中大部分类的基类。

除了NSObject之外的另一个基类——NSProxy,NSProxy实现被根类要求的基础方法,包括定义NSObject协议。

然而,作为抽象类,它不实现初始化方法,并且会在收到任何它不响应的消息时引发异常。因此,具体子类必须实现一个初始化或者创建方法,并且
重写- (void)forwardInvocation:(NSInvocation *)invocation;
和- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel方法,
来转发它没实现的方法。这也是NSProxy的主要功能,负责把消息转发给真正的target的代理类,NSProxy正是代理的意思。



13.NSPointerArray

几个特点:

  • NSPointerArray可以存放nil
  • 可以插入删除nil
  • count 属性可以设置 (用nil占位)
  • 可以初始化以保持对对象的强引用或弱引用,也可以根据NSPointerFunctionsOptions定义的任何内存或个性选项进行初始化
  • NSCopying和NSCoding协议仅在指针数组初始化为保持对对象的强引用或弱引用时才适用
  • 遵循 NSFastEnumeration,可以通过 for...in 来进行遍历