欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

AFNetworking源码解析与面试考点思考

程序员文章站 2022-07-06 10:10:10
最近重读了AFNetworking 3.x的源码,算是温故而知新吧。也梳理了一些优秀的代码细节和面试考点,罗列下来,发现这个库小而精致,简直初学者的宝藏库。 开源库怎么看? 先说个题外话,阅读优质的开源代码库,绝对是程序员们快速提升自我的有效途径,而怎样高效率的去阅读源码同样也是一个问题,不知道有没 ......

最近重读了afnetworking 3.x的源码,算是温故而知新吧。也梳理了一些优秀的代码细节和面试考点,罗列下来,发现这个库小而精致,简直初学者的宝藏库。

开源库怎么看?

先说个题外话,阅读优质的开源代码库,绝对是程序员们快速提升自我的有效途径,而怎样高效率的去阅读源码同样也是一个问题,不知道有没有人和我之前一样,碰到过读倒是读了,但总感觉收获不大的情况。

这里分享一下我的一些读码经验:

  1. 多思考,多抛出问题,比如说

    • 整体的代码结构是怎样的?类与类之间的关系是怎样的?为什么要这么设计?
    • 代码有没有涉及到多线程,其线程模型是怎样的?哪类问题可以适用这种多线程的方案?
    • 代码中使用了哪些设计模式?具体是怎么实现的?
  2. 也可以关注代码细节,遇到不熟悉的用法不要放过,多刨根究底才能夯实基础

    关于afnetworking的一些优秀代码细节,我这里也整理了一部分,可以查阅后文

  3. 一定要记笔记和总结,能分享更好。

    参考费曼学习法,我认为这一点是最好的加深理解和强化记忆的手段。随着年龄的增大,记忆力会有所衰退,有个笔记能够回顾,能节约大把再次记忆的时间。此外,多与人分享还能够提升自己的影响力,与人交流验证,也能够为自己查缺补漏。

afnetworking 3.x的代码结构

还是说回到afnetworking这里,af的代码结构大部分人应该都了解,这里我简单介绍下。afnetworking 3.x的代码结构比2.x要简单许多,主要也得益于苹果优化了网络相关的api,整体代码有这么几部分:

  • afurlsessionmanager/afhttpsessionmanager

    这里就是af代码的核心了,主要负责网络请求的发起,回调处理,是在系统网络相关api上的一层封装。大部分逻辑是在afurlsessionmanager里面处理的,afhttpsessionmanager则是专为http请求提供了一些便利方法。如果需要扩展其他协议的功能(比如ftp协议),可以考虑从afurlsessionmanager创建一个子类。

  • afurlrequestserialization/afurlresponseserialization

    这两兄弟主要处理一些参数序列化相关的工作。afurlrequestserialization是将传入的参数构造成nsurlrequest,比如自定义的header,一些post或者get参数等等。 afurlresponseserialization主要是将系统返回的nsurlresponse处理成我们需要的responseobject,比如json、xml、image等等

  • afsecuritypolicy

    处理https相关的公钥和验证逻辑。目前由于苹果ats的开启,基本https已经成为标配。虽然通常直接使用ca来验证服务器公钥的情况下,不需要我们额外做什么配置。但是从这里出发,顺便考察一下https相关的知识点,感觉也比较常见,具体面试题可看下文

  • afnetworkreachabilitymanager

    这个其实是比较独立的一个模块了,提供获取当前网络状态的功能。

  • uikit+afnetworking

    这里主要是通过category来提供了一下uikit的便利方法

af的一些优质代码细节

仔细瞅瞅代码之后,发现常见的oc基础知识在af里面都有具体应用,挺多还是面试题考点,这里也是做个记录和梳理。

  • 单例的创建方法
+ (instancetype)defaultinstance {
    static afimagedownloader *sharedinstance = nil;
    static dispatch_once_t oncetoken;
    dispatch_once(&oncetoken, ^{
        sharedinstance = [[self alloc] init];
    });
    return sharedinstance;
}

必知必会,通过dispatch_once来保证多线程调用时,只有一个实例被创建。

  • dispatch_sync与dispatch_barrier_sync配合解决并行读串行写问题
// 注意是并行队列
self.requestheadermodificationqueue = dispatch_queue_create("requestheadermodificationqueue", dispatch_queue_concurrent);

// 串行写,注意barrier block的具体执行时机
dispatch_barrier_sync(self.requestheadermodificationqueue, ^{
    [self.mutablehttprequestheaders setvalue:value forkey:field];
});

// 并行读,注意和上面写操作同时执行时的执行顺序
nsdictionary __block *value;
dispatch_sync(self.requestheadermodificationqueue, ^{
    value = [nsdictionary dictionarywithdictionary:self.mutablehttprequestheaders];
});

必知必会,gcd使用barrier来处理并行读串行写问题的具体用法

  • weakself与strongself的用法
__weak __typeof(self)weakself = self;
afnetworkreachabilitystatuscallback callback = ^(afnetworkreachabilitystatus status) {
    __strong __typeof(weakself)strongself = weakself;

    strongself.networkreachabilitystatus = status;
    if (strongself.networkreachabilitystatusblock) {
        strongself.networkreachabilitystatusblock(status);
    }

    return strongself;
};

必知必会,weakself避免循环引用,strongself保证block内部执行过程中self不会被释放

  • 其他用法

    nssecurecoding、kvo、swizzle、nsstream、nsprogress、代码注释、pragma mark等等

afnetworking的可能面试考点

前面提到阅读开源库时,要多思考多提问题,这里也结合一些面试考题来梳理下

afnetworking 2.x怎么开启常驻子线程?为何需要常驻子线程?

这个知识点应该是af2.x版本面试官比较喜欢问的了,af2.x版本有个细节,通过runloop开启了一个常驻子线程,具体代码是这样的:

+ (void)networkrequestthreadentrypoint:(id)__unused object {
    @autoreleasepool {
        [[nsthread currentthread] setname:@"afnetworking"];

        nsrunloop *runloop = [nsrunloop currentrunloop];
        [runloop addport:[nsmachport port] formode:nsdefaultrunloopmode];
        [runloop run];
    }
}

+ (nsthread *)networkrequestthread {
    static nsthread *_networkrequestthread = nil;
    static dispatch_once_t oncepredicate;
    dispatch_once(&oncepredicate, ^{
        _networkrequestthread = [[nsthread alloc] initwithtarget:self selector:@selector(networkrequestthreadentrypoint:) object:nil];
        [_networkrequestthread start];
    });

    return _networkrequestthread;
}

首先,我们要了解为何要开启常驻子线程?

nsurlconnection的接口是异步的,然后会在发起的线程回调。而一个子线程,在同步代码执行完成之后,一般情况下,线程就退出了。那么想要接收到nsurlconnection的回调,就必须让子线程至少存活到回调的时机。而af让线程常驻的原因是,当发起多个http请求的时候,会统一在这个子线程进行回调的处理,所以干脆就让其一直存活下来。

上面说的一般情况,子线程执行完任务就会退出,那么什么情况下,子线程能够继续存活呢?这就涉及到第二个问题了,af是如何开启常驻线程的,这里实际上考察的是runloop的基础知识。

关于runloop,推荐看下《ios底层原理总结 - runloop》,个人觉得讲得非常详尽。这里简单来说,当runloop发现还有source/timer/observer的时候,runloop就不会退出。所以af这里就通过给当前runloop添加一个nsmachport,这个port实际上相对于添加了一个source事件源,这样子线程的runloop就会一直处于循环状态,等待别的线程向这个port发送消息,而实际上af这里是没有消息发送到这个port的。

除了af的这种处理方式,实际上苹果也提供了回调线程的解决方案:

// nsurlconnection
- (void)setdelegatequeue:(nullable nsoperationqueue*) queue

// nsurlsession
+ (nsurlsession *)sessionwithconfiguration:(nsurlsessionconfiguration *)configuration delegate:(nullable id <nsurlsessiondelegate>)delegate delegatequeue:(nullable nsoperationqueue *)queue;

苹果提供了接口,可以让你制定一个operationqueue供回调执行。所以af3.x的时候,就直接创建了一个并发度为1的队列,来处理回调。

扩展问题:

既然提到了runloop,可以考虑一下runloop的扩展问题:

  • 子线程默认有runloop吗?runloop创建和销毁的时机又是什么时候呢?
  • runloop有哪些mode呢?滑动时发现定时器没有回调,是因为什么原因呢?
  • 除了上面这种addport来保活线程的方法,有无其他方法呢?

afurlsessionmanager与nsurlsession的关系,每次都需要新建mananger吗?

我们如果仔细查看代码,应该就能得出这样的结论:manager与session是1对1的关系,af会在manager初始化的时候创建对应的nsurlsession。同样,af也在注释中写明了可以提供一个配置好的manager单例来全局复用。

那么复用manager实际上就是复用了session,而复用session可以带来什么好处呢?

其实ios9之后,session就开始支持http2.0。而http2.0的一个特点就是多路复用(可参考《http系列(二) http2中的多路复用》)。所以这里复用session其实就是在利用http2.0的多路复用特点,减少访问同一个服务器时,重新建立tcp连接的耗时和资源。

官方文档也推荐在不同的功能场景下,使用不同的session。比如:一个session处理普通的请求,一个session处理background请求;1个session处理浏览器公开的请求,一个session专门处理隐私请求等等场景。

afsecuritypolicy如何避免中间人攻击?

现在,由于苹果ats的策略,基本都切到https了,https的基本原理还是需要了解一下的,这里不做介绍,需要的可以google查阅一下相关文章。

通常,首先我们要了解中间人攻击,大体就是黑客通过截获服务器返回的证书,并伪造成自己的证书,通常我们使用的charles/fiddler等工具实际上就可以看成中间人攻击。

解决方案其实也很简单,就是ssl pinningafsecuritypolicyafsslpinningmode就是相关设置项。

ssl pinning的原理就是需要将服务器的公钥打包到客户端中,tls验证时,会将服务器的证书和本地的证书做一个对比,一致的话才允许验证通过。

typedef ns_enum(nsuinteger, afsslpinningmode) {
    afsslpinningmodenone,
    afsslpinningmodepublickey,    // 只验证证书中的公钥
    afsslpinningmodecertificate,    // 验证证书所有字段,包括有效期之内
};

由于数字证书存在有效期,内置到客户端后就存在失效后导致验证失败的问题,所以可以考虑设置为afsslpinningmodepublickey的模式,这样的话,只要保证证书续期后,证书中的公钥不变,就能够通过验证了。

扩展:

用了ssl pinning就安全了吗?

更多文章:

2020年ios大厂面试题总结(一)
ios学习栈(将持续更新)上
阿里、字节:一套高效的ios面试题

推荐