vue-cli3定制脚手架命令行工具实践

前言

目前大多数脚手架项目都是在用yeoman开发,但是逐渐暴露除了一些问题:
* 团队中的脚手架都是采用个人维护,出于维护成本的考虑,脚手架的更新会相对较慢,滞后于前端技术的发展。
* 脚手架生成的前端项目的webpack配置,也由于需求不同导致参差不齐。
vue-cli就很大程度上解决了上面的问题,它提供了最基础的配置,大家可以开箱即用,同时也可以最大程度上的定制化,也就是之前讲到的预设

原理

原理很简单,就是将vue-cli命令进行封装。制定一个特殊命令,将定制好的 preset 交给 vue-cli处理,而其他情况则完全透转给 vue-cli。

实践

命令行工具的核心就是,创建个命令 create ,核心就是当执行 my-cli create demo 的时候,就会把定义好的preset 传给 vue-cli处理。

#!/usr/bin/env node
const path = require('path');
const preset = path.resolve(__dirname,'my-preset');

const program = require('commander');
const execa = require('execa');

program
  .version(require('./package').version)
  .description('基于vue-cli的定制脚手架')
  .usage(' [options]');

program
  .command('create ')
  .description('使用定制preset创建vue-cli项目')
  .action(function (project) {
    //最核心就是这里
    let command = `vue create ${project} --preset ${preset}`;
    const child = execa.shell(command, {
        stdio: 'inherit'
    });
  })

//除了专有的create命令,其他的命令都转交给vue-cli
program
  .command('*')
  .action(function(){
      let command = process.argv.slice(2);
      command.unshift('vue');

      const child = execa.shell(command.join(' '),{
          stdio:'inherit'
      });
  });

program.parse(process.argv);

这样在想要使用自己的预设的时候,就使用自己定制的命令,同时也不影响vue-cli本来的命令。

GraphQL Prisma

本文将介绍 Prisma 的适用场景,拥有的优点以及如何将它融入到你的技术栈中。

Prisma 是什么 ?

Prisma 是在你的应用架构中用来替代传统 ORM 框架的数据层框架(data layer)。

该 data layer 由以下几个组件组成:

  • 扮演数据库代理角色的 Prisma server

  • 运行在 Prisma server 上的高性能的查询引擎(query engine),用于生成真实的数据库查询请求

  • 连接到 Prisma server 的客户端 Prisma client

  • 实时的事件系统,让你可以订阅相关的数据库事件

适用场景

在你处理各类数据库操作的场景中,在任何上下文里,Prisma 都是一个非常有用的工具。

创建 GraphQL 服务器

Prisma 是创建 GraphQL 服务器的完美工具。Prisma client 能够很好的兼容 Apollo 框架生态,拥有默认的对 GraphQL subscriptions 的支持,以及 Relay 风格的分页支持,同时提供端对端类型安全的以及内置的dataloader来解决 N+1 问题。

创建 REST APIs

Prisma 非常适合用来创建 REST APIs,主要用于取代传统的 ORM 框架。它拥有类型安全、先进API以及自由读写关系型数据等诸多优点。

CLIs 命令行, Scripts 脚本,Serverless Functions 及其它

Prisma 拥有极其自由的 API,让其非常适合处理各类使用场景。当你需要同一个或多分数据库进行会话时,Prisma 将会在数据库workflows简化方面提供巨大的帮助。

为什么用 Prisma?

简单的数据库工作流 database workflows

Prisma 的最终目标是去除你的应用中的复杂的通用数据库工作流同时简化数据库访问。

  • 类型安全的数据库访问,得益于已配置的自动生成的 Prisma client

  • 处理关系型数据以及事务的简单而强大的API

  • Prisma 同时对多个数据库的统一的访问,因此大大降低了跨数据库工作流程的复杂性

  • 数据库实时数据流以及事件系统,能够确保你能够获取到数据库中发生的所有重要事件的更新

  • 基于使用 GraphQL schema definition language(SDL)表示的声明式数据类型datamodel 的自动数据库迁移方案

  • 其它数据库工作流,如数据导入/导出等

实时的数据库层

一些数据库,例如 RethinkDB 或者 DynamoDB 提供了开箱即用的实时API。这样的API允许客户端订阅数据库中发生的任何更改。然而,绝大多数传统数据库不提供这样的实时API,并且手动实现它非常复杂。Prisma为每个受支持的数据库提供实时API,允许您订阅任何数据库事件,例如创建,更新或删除数据。

端到端安全性

以类型安全的方式编程是现代应用程序开发的默认设置。以下是安全类型的一些核心优势:

  • 信心:由于静态分析和编译时错误检查,开发人员可以对代码充满信心。

  • 开发体验:在明确定义数据类型时,开发人员开发体验更好。类型定义是IDE功能的基础,如智能自动补全或定义跳转。

  • 代码生成:在开发工作流程中利用代码生成很容易,以避免编写样板。

  • 跨系统协定:类型定义可以跨系统共享(例如,在客户端和服务器之间),并用作定义相应接口/ API的协定。

端到端类型安全是指从客户端到数据库在整个堆栈中具有类型安全性。端到端类型安全体系结构可能如下所示:

  • 数据库:Prisma提供强类型数据库层,datamodel定义了存储在数据库中的数据类型

  • 应用程序服务器:应用程序服务器定义自己的schema(例如,使用GraphQL或OpenAPI / Swagger),它可以重用或转换数据库中的数据类型。应用程序服务器需要使用类型安全的语言(例如TypeScript,Scala,Go)编写。

  • 客户端:了解应用程序服务器架构的客户端可以在构建时验证API请求和潜在响应。

整洁分层式架构

在开发应用程序服务器时,最复杂的是在同步,查询优化 / 性能和安全性等方面,实现安全且组织良好的数据库访问。当涉及多个数据库时,这变得更加复杂。

解决这个问题的一个常见解决方案是引入专用数据访问层(DAL),它将数据库访问的复杂性抽象出来。DAL的API由应用程序服务器使用,允许API开发人员只需简单地思考他们需要什么数据,而不必担心如何从数据库安全地和高效地检索它。

img

使用 DAL 架构可以确保关注点分离,从而提高代码的可维护性和复用性。具有某种数据库抽象(无论是简单的 ORM 库还是独立的基础架构组件)是小型应用以及大规模运行的应用的最佳实践。它确保应用服务器能够以安全而高效的方式与您的数据库进行通信。

Prisma 是一个自动生成的 DAL,它遵循了其它行业领先的DAL(例如 Twitter 的 Strato 或 Facebook 的 TAO)相同的原则,同时可以轻易的在小型的应用中使用。

Prisma 让你在一开始就使用干净的体系结构启动项目,并让你免于编写大量应用服务器和数据库通信的模块。

如何让 Prisma 融入到您的技术栈中?

Prisma 是一个位于数据库之上的独立基础架构组件。您可以在应用服务器中使用 Prisma client(各种类型的语言支持)来连接到 Prisma。

这使您可以通过简单而流行的API与数据库通信,并确保高性能和安全的数据库访问。

数据处理 · 初步的认识关系图(二)

标签(空格分隔): 关系图


操作图形
在上一篇《数据处理 · 初步的认识关系图》中完成了关系图谱的建立后,然后要将其运用起来以配合算法来解决一些我们所面临的问题。但在实现算法之前我们需要先为关系图谱定义一些操作方法,如获取某一个顶点、遍历所有顶点、遍历所有边等。

1、获取某一个顶点

在上一篇《数据处理 · 初步的认识关系图》中关系图类中使用了 vertexIds 存储顶点的标识符和使用 vertices 来存储顶点对象。那么要获取图形中的某一个顶点,首先要确保在 vertexIds 中存在该节点标识符,否则就直接返回 null。然后再从 vertices 中获取该节点的实例对象以返回。

//Graph是总的关系图类
class Graph { // ... 
    getVertex(vertexId) { 
        if (!_.includes(this.vertexIds, vertexId)) { 
            return null 
        } 
        return this.vertices[vertexId] 
    } 
} 

2、遍历顶点/边

虽然在 JavaScript 中默认所使用的数组都是自带有序特性的,但在关系图谱的定义中,顶点之间并不存在顺序。所以不会允许对图的对象中的顶点进行直接的循环操作,而采用回调函数的方式进行循环遍历。

class Graph { // ... 
    //点
    eachVertices(callbackFunc) { 
        const self = this 
        return self.vertexIds.forEach(function(vertexId) { 
            return callbackFunc(self.vertices[vertexId]) 
        }) 
    } 
    //边
    eachEdges(callbackFunc) { 
        const self = this 
        return self.edgeIds.forEach(function(edgeId) { 
            return callbackFunc(self.edges[edgeId]) 
        }) 
    } 
} 

3、Degree

Degree 在树形结构的节点中表示的是某一个节点的子节点数量,而因为在关系图中的顶点并不存在“子节点”或“子顶点”的概念,取而代之的则是相邻顶点。而相邻顶点的数量就等于与该顶点相连的边的数量。那么要获取相邻边的数量则首先需要定义一个方法以传入顶点标识符并得到相邻边数组。

class Graph { 
    // ... 
    getEdgesByVertexId(vertexId) { 
        if (!_.includes(this.vertexIds, vertexId)) { 
            return [] 
        } 
        if (!_.has(this.edgeRelations, vertexId)) { 
            return [] 
        } 
        const self = this 
        return self.edgeRelations[vertexId].map(function(edgeId) { 
            return self.edges[edgeId] 
        }) 
    } 
} 

注:某顶点的度是得到相邻边后返回其长度则便是该顶点的度。

    class Graph { 
        // ... 
        //顶点的度
        degree(vertexId) { 
            return this.getEdgesByVertexId(vertexId).length 
        } 
    } 

总结:2篇是关系图的建立和操作其顶点/边,顶点的度定义,其实关系图有无向图和有向图。

Istio服务治理介绍

1. 什么是istio?

Istio是由Google、IBM和Lyft开源的微服务管理、保护和监控框架,目前已经更新到v1.2.2版本。Istio为希腊语,意思是”起航“。官方对 istio 的介绍浓缩成了一句话:

An open platform to connect, secure, control and observe services.

翻译过来,就是”连接、安全加固、控制和观察服务的开放平台“。开放平台就是指它本身是开源的,服务对应的是微服务,也可以粗略地理解为单个应用。

中间的四个动词就是 istio 的主要功能:

  • 连接(Connect):智能控制服务之间的调用流量,能够实现灰度升级、AB 测试和红黑部署等功能
  • 安全加固(Secure):自动为服务之间的调用提供认证、授权和加密
  • 控制(Control):应用用户定义的 policy,保证资源在消费者中公平分配
  • 观察(Observe):查看服务运行期间的各种数据,比如日志、监控和 tracing,了解服务的运行情况

2. 为什么要使用 Istio?

Istio 提供一种简单的方式来为已部署的服务建立网络,该网络具有负载均衡、服务间认证、监控等功能,只需要对服务的代码进行一点或不需要做任何改动。想要让服务支持 Istio,只需要在额们环境中部署一个特殊的 sidecar 代理,使用 Istio 控制平面功能配置和管理代理,拦截微服务之间的所有网络通信。使用istio的进行微服务管理有如下功能:
– HTTP、gRPC、WebSocket 和 TCP 流量的自动负载均衡。
– 通过丰富的路由规则、重试、故障转移和故障注入,可以对流量行为进行细粒度控制。
– 可插入的策略层和配置 API,支持访问控制、速率限制和配额。
– 对出入集群入口和出口中所有流量的自动度量指标、日志记录和追踪。
– 通过强大的基于身份的验证和授权,在集群中实现安全的服务间通信。

Istio 旨在实现可扩展性,满足各种部署需求。

3. 架构

Istio架构分为控制层和数据层:

  • 数据层:由一组智能代理(Envoy)作为sidecar部署,协调和控制所有microservices之间的网络通信。
  • 控制层:负责管理和配置代理路由流量,以及在运行时执行的政策。

4. Envoy

Istio使用Envoy代理的扩展版本,该代理是以C++开发的高性能代理,用于调解service mesh中所有服务的所有入站和出站流量。 Istio利用了Envoy的许多内置功能,例如:
– 动态服务发现
– 负载平衡
– TLS终止
– HTTP/2&gRPC代理
– 熔断器
– 健康检查、基于百分比流量拆分的灰度发布
– 故障注入
– 丰富的度量指标

Envoy在kubernetes中作为pod的sidecar来部署。 这允许Istio将大量关于流量行为的信号作为属性提取出来,这些属性又可以在Mixer中用于执行策略决策,并发送给监控系统以提供有关整个mesh的行为的信息。 Sidecar代理模型还允许你将Istio功能添加到现有部署中,无需重新构建或重写代码。

5. Pilot

Pilot 为 Envoy sidecar 提供服务发现功能,为智能路由(例如 A/B 测试、金丝雀部署等)和弹性(超时、重试、熔断器等)提供流量管理功能。它将控制流量行为的高级路由规则转换为特定于 Envoy 的配置,并在运行时将它们传播到 sidecar。

Pilot 将平台特定的服务发现机制抽象化并将其合成为符合 Envoy 数据平面 API 的任何 sidecar 都可以使用的标准格式。这种松散耦合使得 Istio 能够在多种环境下运行(例如,Kubernetes、Consul、Nomad),同时保持用于流量管理的相同操作界面。

6. Mixer

Mixer负责在service mesh上执行访问控制和使用策略,并收集Envoy代理和其他服务的遥测数据。代理提取请求级属性,发送到mixer进行评估。有关此属性提取和策略评估的更多信息,请参见Mixer配置。 混音器包括一个灵活的插件模型,使其能够与各种主机环境和基础架构后端进行接口,从这些细节中抽象出Envoy代理和Istio管理的服务。

7. Citadel

Citadel 通过内置身份和凭证管理赋能强大的服务间和最终用户身份验证。可用于升级服务网格中未加密的流量,并为运维人员提供基于服务标识而不是网络控制的强制执行策略的能力。从 0.5 版本开始,Istio 支持基于角色的访问控制,以控制谁可以访问您的服务,而不是基于不稳定的三层或四层网络标识。

8. Galley

Galley 代表其他的 Istio 控制平面组件,用来验证用户编写的 Istio API 配置。随着时间的推移,Galley 将接管 Istio 获取配置、 处理和分配组件的顶级责任。它将负责将其他的 Istio 组件与从底层平台(例如 Kubernetes)获取用户配置的细节中隔离开来

总结:

Istio 的出现为微服务架构减轻了很多的负担,开发者不用关心服务调用的超时、重试、流控的等实现,服务之间的安全、授权也得到了保证;集群管理员也能够很方便地发布应用(AB 测试和灰度发布),并且能清楚看到整个集群的运行情况。

但是这并不表明有了 istio 就可以高枕无忧了,istio 只是把原来分散在应用内部的复杂性统一抽象出来放到了统一的地方,并没有让原来的复杂消失不见。因此我们需要维护 istio 整个集群,而 istio 的架构比较复杂,尤其是它一般还需要架在 kubernetes 之上,这两个系统都比较复杂,而且它们的稳定性和性能会影响到整个集群。因此再采用 isito 之前,必须做好清楚的规划,权衡它带来的好处是否远大于额外维护它的花费,需要有相关的人才对整个网络、kubernetes 和 istio 都比较了解才行。

参考

  • https://istio.io/docs/concepts/what-is-istio/

数据库隔离级别剖析

前言

在线应用业务中,数据库是一个非常重要的组成部分,特别是现在的微服务架构,我们为了水平扩展能力,倾向于将状态都存储在数据库中,这样要求数据库高性能并且正确地处理请求。这几乎是一个不可能达到的要求,使得数据库的设计者们定义了隔离级别这一个概念,在高性能与正确性之间提供了一个缓冲地带,明确地告诉使用者,我们提供正确性差一点但是性能好一点的模式和正确性好一点单身性能差一点的模式,使用者按照你们的业务场景来选择使用吧。

本质

本质来说,隔离级别是定义数据库并发控制的。在我们应用程序的开发中,我们通常利用锁来进行并发控制,确保临界区的资源不会多个线程同时进行读写,这对应与数据库的隔离级别为可串行化(最高的隔离级别)。现在发现离级别是和我们日常开发很近的一个概念了吧,那么现在肯定会有一个问题,为什么应用程序可能提供可串行化的隔离级别,而数据库不能提供呢?其根本的一个原因是应用程序都是内存操作,数据库基本都需要持久化到磁盘,内存操作和磁盘操作的耗时是好几个数量级的差别,锁的临界区变长,会导致竞争变的激烈,程序的性能会大大降低。

隔离级别

数据库的隔离级别,目前主流的定位为以下四个等级:读未提交,读已提交,可重复读,可串行化。但是由于各个数据库的具体实现各不相同,所以我们先不讨论隔离级别的定义,直接从各个隔离级别会带来的异常情况来分析隔离级别的定义。

异常

从读未提交到可串行化,数据库可能出现的异常为:
脏写
事务a覆盖了其他事务尚未提交的写入。

脏读
事务a读到了其他事务尚未提交的写入。

读倾斜
事务a在执行过程中,对某一个值在不同的时间点读到了不同的值。即为不可重复读。

更新丢失
两个事务同时执行读-修改-写入操作序列,出现了其中一个覆盖了另一个的写入,但是没有包含对方最新的值的情况,导致了被覆盖部分修改数据发生了丢失。

幻读
事务先查询了某些符合条件的数据,同时另一个事务执行写入,改变了先前的查询结果。

写倾斜
事务先查询数据库,根据返回的结果而作出某些决定,然后修改数据库。在事务提交的时候,支持决定的条件不再成立。写倾斜是幻读的一种情况,由于读-写事务冲突导致的幻读。写倾斜也可以看做一种更广义的更新丢失问题。即如果两个事务读取同一组对象,然后更新其中的一部分:不同的事务更新不同的对象,可能发生写倾斜;不同的事务跟新同一个对象,则可能发生脏写或者更新丢失。

异常避免

对应四个隔离级别,我们分别来看看他们有什么异常情况,以及怎么通过应用层的优化来避免该异常的发生。

对于脏写,几乎所有的数据库都可以防止,我们用的mysql和tidb更是没有问题,所以不讨论脏写的情况;

对于脏读,提供读已提交 隔离级别以及以上隔离级别的都可以防止问题的出现,如果业务中不能接受脏读,那么隔离级别最少在读已提交 或者以上;

对于读倾斜,可重复读 隔离级别以及以上隔离级别的都可以防止问题的出现,如果业务中不能接受脏读,那么隔离级别最少可重复读 或者以上;

对于 更新丢失,幻读,写倾斜,如果只通过数据库隔离级别来处理的话,那么只能才有 可串行化 的隔离级别才能防止问题的出现,然后生产环境中,我们是不可能开启 可串行化 隔离级别的,那么是数据库直接不支持,要么是数据库支持,但是性能太差。因而在时机开发中,我们只能中可重复读的隔离级别的基础上,通过一些其他的手段来防止问题的发生。

怎么避免更新丢失

如果数据库提供原子写操作,那么一定要避免在应用层代码中完成“读-修改-写”的操作,直接通过数据库的原子操作来执行,这样就可以避免更新丢失的问题。数据库的原子操作例如关系数据库中的 udpate table set value=value+1 where key=*,mongodb也提供类似的操作。 数据库的原子操作一般通过独占锁来实现,相当于少可串行化的隔离级别,所以不会有问题。不过在使用ORM框架的时候,就很容易在应用层代码中完成“读-修改-写”的操作,导致无法使用数据库的原子操作。
另外一个情况,如果数据库不支持原子操作,或者在某一些场景,原子操作不能处理的时候,可以通过对 查询结果 显示加锁来解决。对于mysql来说,就是 select for update,通过for update告诉数据库,我查询出来的数据行一会是需要跟新的,需要帮我加锁防止其他的事务也来读取更新导致更新丢失。
一种更好的避免更新丢失的方式是数据库提供 自动检测 更新丢失的机制。数据库先让事务都并发执行,如果检测到有更新丢失的风险,直接中止当前事务,然后业务层在重试即可。目前PostgreSQL的可重复读,Oracle的可串行化等都提高自动检测 更新丢失的机制,但是mysql的 InnoDB的可重复读并不支持。
有某一些情况下,还可以通过 原子比较和设置来实现,例如:update table set value=newvalue where id=* and value=oldvalue。但是该方式有一个问题,如果where条件的判断是基于某一个旧快照来执行的,那么where的判断是没有意义的。所以如果要采用 原子比较和设 来避免更新丢失,那么一定要确认 数据库 比较-设置 操作的安全运行条件。

怎么避免幻读中的写倾斜

在 怎么避免更新丢失 我们提供了很多种方式来避免 更新丢失,那么在 写倾斜 的时候可以使用吗?
1、原子操作上不行的,因为涉及到多个对象的更新;
2、所以的数据库几乎都没有自动 自动检测 写倾斜的机制;
3、数据库自定义的约束功能对于多个对象也基本不支持;
4、显式加锁 方式上可以的,通过select for update,可以确保事务以可串行化的隔离级别,所以这个方案上可行的。但是不是对于所有的方式都可以使用,如果select for update 在select的时候不能查询到数据,这个时候数据库无法对数据进行加锁。例如:
在订阅会议室的时候,select的时候会议室还没有被订阅,所以查询不到,数据库也没有办法进行加锁,update的时候,多个事务都可以update成功。
所以,显式加锁对于写倾斜不能适用的原因是因为在select阶段没有查询到临界区的数据,导致无法加锁。所以在这种情况下,我们可以人为的引入用于加锁的数据,然后通过 显式加锁 来避免 写倾斜的问题。
比如在订阅会议室的问题中,我们为所有的会议室的所有时间都创建好数据,每一个时间-会议室一条数据,这个数据没有其他的意义,只是用来select for update的时候由于select 查询到数据,用于数据库来加锁。
另外一种方式是在数据库提供可串行化隔离级别,并且性能满足业务要求时,直接使用可串行化的隔离级别。

Binder简介

整体设计

屏幕快照 2019-06-30 下午4.13.30.png

Android的IPC通过binder驱动来实现的,应用进程通过访问/dev/binder文件来进入驱动程序。

getSystemService("activity");

编程时通过服务名称可以获取到一个服务的代理对象,然乎就能通过这个服务代理对象访问运行在其他进程的服务了

使用binder通信机制,驱动程序和应用进程会映射同一块物理内存,使得IPC的过程中只需要拷贝一次数据。客户端通过/deb/binder访问驱动,并传入通信数据,驱动程序拿到数据后,会执行一次拷贝,拷贝到相应服务进程对应的缓冲区,服务进程就可以从缓冲区直接拿到数据了。

驱动程序

驱动程序中定义了一些数据结构用来保存各个进程的状态,当进程打开/dev/binder文件时,驱动程序将会为该进程分配一个名为PROC的数据结构,用来表示该进程。

其他用来处理IPC的数据一般都放在该数据结构上,比如线程池,缓冲区等。binder服务会分配线程用来处理IPC请求,每分配一个线程,就会将该线程的信息(pid,优先级等),通过对/dev/binder的访问,记录到内核对应该进程的PROC数据结构中。同样的,用户进程使用mmap映射/dev/binder时,内核会分配内核缓冲区,并将缓冲区地址记在PROC数据结构中。

驱动程序需要处理缓存区的管理,mmap只会执行一次,会分配一个大的缓冲区,每一次IPC通信都会用到一小块内存,用来存放通信的协议数据等,所以驱动程序会处理内存的查找,剪裁,合并等。

驱动程序也管理了线程池,当有IPC请求传给某个进程时,会从该进程的PROC结构中找到一个合适的线程,将请求封装成一个任务交给该线程或进程的等待队列,并唤醒线程。所以驱动个程序需要处理线程的唤醒,阻塞,任务队列的维护。

应用进程之间通过Binder驱动通行时,需要能互相标识,这样Binder驱动收到请求才知道要将请求发给哪个进程处理,这是通过一个int值的句柄来标识的。当发起请求时,将通信数据和句柄发给Binder驱动,驱动根据句柄找到对应进程的PROC数据结构,然后将通信数据封装成任务放到该PROC相应的任务队列中,唤醒或等待线程进行处理。

ServiceManager

回顾发起IPC通信的第一步,需要知道服务进程在Binder驱动中对应的句柄,而服务对应的句柄是在Binder驱动中为该进程分配的,所以这里需要一个特殊的服务进程,可以通过约定的句柄获得,该进程就是ServiceManager所在的进程,通过句柄值0就可以发起与ServiceManager的请求。

调用getService获取的服务是注册在ServiceManager中的,当需要获取服务代理对象时,进程想binder驱动写入通信数据,数据包含获取服务的名称以及句柄0,驱动程序根据句柄值0就会将请求发给ServiceManager的进程。ServiceManager拿到服务名称后,就会将代理对象返回。

返回给请求服务进程的实际上是一个句柄,拿到句柄后就可以使用该句柄,通过Binder驱动与相应的进程进行通信了。

binder客户端打开binder

public static abstract class Stub extends android.os.Binder implements com.test.liuxin.test.IMyAidlInterface {
        private static final java.lang.String DESCRIPTOR = "com.test.liuxin.test.IMyAidlInterface";
        static final int TRANSACTION_fun = (android.os.IBinder.FIRST_CALL_TRANSACTION +
            0);

        public Stub() {
            this.attachInterface(this, DESCRIPTOR);
        }

        public static com.test.liuxin.test.IMyAidlInterface asInterface(
           ...
        }

        @Override
        public android.os.IBinder asBinder() {
            return this;
        }

        @Override
        public boolean onTransact(int code, android.os.Parcel data,
            android.os.Parcel reply, int flags)
            throws android.os.RemoteException {
            ...
            this.fun(_arg0, _arg1, _arg2, _arg3, _arg4, _arg5);
            ...
        }

回忆AIDL的使用,服务的提供这需要继承自动生成的Stub对象。自动生成的stub类继承了binder类,binder类是系统Binder库提供的类,binder库将会为我们处理与binder驱动交互的细节,我们只需要关系具体的业务即可。

Binder对象最终会通过一个ProcessState对象来处理与binder驱动的通信,该对象是一个进程的单例对象。当第一次访问binder相关的接口时,ProcessState会为我们调用open打开/dev/binder,该过程会为我们创建Binder驱动中表示该进程的PROC等数据结构,以及mmap映射/dev/binder,为该进程分配内核缓冲区。当需要进程通信时,该类会为我们封装通信数据和协议,来传给binder驱动处理。

当收到进程间通信请求时,会调用onTransact接口,如源码所示,我们只需要处理接口的实现即可,其他的都交给自动生成的代码处理,会为我们封装进程通信数据和与binder驱动交互。

注册到Binder中

关于代理对象如何返回,我们首先详细描述一下Service的注册过程。

Binder服务进程首先同样通过句柄0,拿到一个ServiceManager的代理对象,使用该句柄0通过Binder驱动与ServiceManager通信。然后以服务名称调用ServiceManager的addService。当Binder驱动将任务放入到ServiceManager对象PROC的等待队列后,会有ServiceManager的线程进行处理。

我们知道,在第一次访问Binder驱动时,Binder驱动会为进程创建PROC数据结构。注册Service的进程获取ServiceManager的代理对象时,就算是访问过Binder驱动了,此时已经创建好了PROC数据结构。Binder驱动收到发给ServiceManager的addService请求时,会将要注册的服务名称,以及发起请求的进程对象的PROC指针(类似)作为参数,创建成一个任务放到ServiceManager对应的PROC的等待队列上。ServiceMnager的线程就会处理该任务,首先会从参数中拿到请求进程的PROC,会创建一个指向其的引用对象,并为该对象分配一个句柄,然后将该句柄和服务名称返回到用户空间。用户空间将该映射保存起来。

在看获取服务的过程。

当ServiceManager收到请求时,会根据服务名称返回一个句柄,从上面的分析可以看到,该句柄可以获取到PROC对象。拿到PROC对象后,binder驱动就会将该PROC对象的指针封装成一个任务放到请求服务进程的PROC的任务队列上。请求服务的线程会处理该任务,并创建一个指向PROC对象的引用对象,并为其分配一个句柄,然后返回给应用进程。应用进程之后就可以通过该句柄发起进程间通信请求与指定的进程通信了。

关于DOM截图的一些事

背景

上上篇文章介绍了使用dom-to-image生成透明图片,然后在生产环境发现会有以下2个问题:
1. 一些移动设备在生成png时会报Tained canvaes may not be exported, 最开始以为是跨域问题,所以将加载的图片内容加了跨域属性,而且将所有http图片地址都转成了https地址(与主文档协议统一),然而并没有解决问题。

查询资料后发现当canvas中含有外部图片或foreignObject时会认为canvas已被污染,浏览器认为可能会泄露用户信息,故不允许导出。详见

  1. 上述问题,通过调用dom-to-image.toSvg可以绕过,然而在移动端,生成的图片却不被认为是一张图片,而会认为是一个文档文件, 在iOS设备上还好,用户依然可以以图片方式保存,只不过保存的图片没有进相册;在android端保存会报保存失败错误

基于以上问题,故考虑更换技术方案:

  1. 前端使用html2canvas库

    使用html2canvas时遇到了3个问题:

  • 如果用户再截图过程中滚动页面,会导致生成的图片内容也会截到滚动过程中的内容(待调研)

  • 如图: border没有显示出来,圆角背景没有透明,图片没有截完全(理论上这些问题都是支持的,但我没有实验成功)

  1. 后端生成

    后端已经有一套html2image截图工具, 但在调研时发现有以下几个问题:

  • 依然是圆角透明问题
  • 抓取时间时机问题,已知服务端抓取需要纯静态页面,所以特地做了一套静态页面(预告: 下次分享静态化相关),然而实际应用中依然有文字错位、图像模糊等问题,而且接口等待时间太长,所以服务端截图可靠性也不是很高。

Puppeteer

puppeteer 是Google在17年和Chrome Headless一起推出的一个工具,详情介绍

它可以自动化执行浏览器的所有操作,并可以进行dom复制、截图、观察控制台等一系列自动化操作

安装

yarn add puppeteer 未翻墙用户会超级慢,因为他会下载整个Chromium

未翻墙用户也可以安装 puppeteer-core , 然后自己搜索下载Chrome Driver, 并指定Chromium 位置即可

另外几个指令可能对未翻墙用户有效:

PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1 # 跳过Chromium下载,跳过后可以手动下载,或者干脆fork到git仓库中

npm set puppeteer_download_host https://npm.taobao.org/mirrors #使用淘宝镜像
npm set chromedriver_cdnurl https://npm.taobao.org/mirrors/chromedriver


启动

而且控制起来超级简单,之前的博文中也有人介绍相关操作,废话不多说, 直接show code。

const browser = await puppeteer.launch({
    headless: true
  });
const page = await browser.newPage();
await page.setViewport({
    isMobile: true,
    width: 375 * 2, // 2倍屏
    height: 812 * 2
})
 await page.goto(url)

这样就开启了一个Chromium, 并打开了一个url。

截屏

await page.waitForSelector('#certificate-img', {
        visible: true
    })
let img = await page.$('#certificate-img')
await img.screenshot({
    path: `files/${uid}.png`,
    omitBackground: true // 允许截图带透明度
})

puppeteer等待有几种方式:

  • waitFor 等待一段时间或者某元素
  • waitForSelector 取值visible/hidden 等待某元素或某XPATH可见或隐藏
  • waitForFunction 等待方法回调,并且可以指定轮询方式
  • waitForNavigation 等待导航栏事件 “load” | “domcontentloaded” | “networkidle0” | “networkidle2
  • waitForXPath 同waitForSelector

截图结果十分完美,所谓所见即所得

多实例

以上截屏操作,在headless:true情况最少需要1s截取一个页面(办公环境下,页面需要ajax请求生产数据),需要截图的页面共有45000+个,这样算起来需要 45000/3600 = 12+小时以上,而且截的图片还要上传CDN, 这个速度实在受不了,而且一定会在高峰期影响到生产用户(离线导数据另说)

所以考虑同时执行多个任务

在node环境开启多进程还是比较简单的:

if (cluster.isMaster) {
    for(let i = 0 ;i  {
        if (message && message.cmd && message.cmd === 'writefile') {
            writeFile(message.content)
        } else {
            console.log(`[Master]# Worker ${worker.id}: ${message}`)
            endTaskNum++
            if (endTaskNum === numCPUs) 
                cluster.disconnect()
            }
        }
        })
    cluster.on('exit', (worker, code, signal) => {
        let index = workers.findIndex(w => worker.id === w.id)
        console.log(`[Master]#${index} Worker ${worker.id} died.`)

        // 进程死掉后自动拉起
        let newWorker = cluster.fork()
        newWorker.send(index)
        workers.splice(index, 1, newWorker)
    })
} else {
    process.on('message', seq => {
        // console.log(`[Worker]# starts calculating...`)
        const start = Date.now()
        const result = doWork(seq)
        console.log(`[Worker]# The result of task ${process.pid} is ${result}, taking ${Date.now() - start} ms.`)
        process.send('My task has ended.')
    })
}

真正的工作进程就是doWork了。因实践过程中经常发现超时的现象,超时后我会主动终止进程,让进程自动重启,当前也会有断点续做的功能,很简单,这里不细说了

本以为可以挂机开工,却遇到了新问题,任务输出写到同一个文件,跑了一会发现日志增加非常缓慢,思考一下,应该是文件锁的原因,既然都在抢文件,那就交给master吧。

if (message && message.cmd && message.cmd === 'writefile') {
    writeFile(message.content)
}

最终整个任务耗时大约4h完成

后续还有洗数据过程,这里不就详述了,全部代码可私信我,由于时间仓促写的有点乱。

NSProxy简介

一.NSProxy定义:

An abstract superclass defining an API for objects that act as stand-ins for other objects or for objects that don’t exist yet. NSProxy是一个抽象的超类,它定义了一个对象的API,用来充当其他对象或者一些不存在的对象的替身。通常,发送给Proxy的消息会被转发给实际对象,或使Proxy加载(转化为)实际对象。NSProxy的子类可以用于实现透明的分布式消息传递(例如,NSDistantObject),或者用于创建开销较大的对象的惰性实例化。
NSProxy的使用

二.NSProxy的使用
1.解决NSTimer CAAnimation 无法释放的问题

在开发过程中经常会用到计时器NSTimer,使用方式有两种

1.   self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerFired) userInfo:nil repeats:YES];
2.  self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerFired) userInfo:nil repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];

我们必须在合适的时机释放NSTimer,一般在 -(void)dealloc,或者-(void)viewWillDisappear:(BOOL)animated这两个方法中调用[self.timer invalidate]释放timer。第二种就是使用NSProxy初始化一个子类

 RDWeakProxy *proxy = [RDWeakProxy proxyWithTarget:self];
    _timer = [NSTimer timerWithTimeInterval:0.1 target:proxy selector:@selector(tick:) userInfo:nil repeats:YES];

第二种就是CAAnimation无法释放的问题

/* The delegate of the animation. This object is retained for the
 * lifetime of the animation object. Defaults to nil. See below for the
 * supported delegate methods. */

@property(nullable, strong) id <CAAnimationDelegate> delegate;

delegate是strong修饰的


CAKeyframeAnimation *zoom = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"]; zoom.delegate = self; zoom.duration = 1.4; zoom.fillMode = kCAFillModeBoth; zoom.removedOnCompletion = NO;

使用上面的写法,self会被强引用,我们必须在合适的时间移除动画才能释放self

[self.layer removeAnimationForKey:@"transform.scale"];

还有一种方案就是使用RDWeakProxy,我们不用关心释放时机,代码如下

    CAKeyframeAnimation *zoom = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
    zoom.delegate = (RDTreasureBoxAlertView *)[RDWeakProxy proxyWithTarget:self];
    zoom.duration = 1.4;
    zoom.fillMode = kCAFillModeBoth;
    zoom.removedOnCompletion = NO;

RDWeakProxy的具体实现,参考了YYKit的方式

- (instancetype)initWithTarget:(id)target {
    _target = target;
    return self;
}

+ (instancetype)proxyWithTarget:(id)target {
    return [[RDWeakProxy alloc] initWithTarget:target];
}

- (id)forwardingTargetForSelector:(SEL)selector {
    return _target;
}

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

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
    return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}

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

- (BOOL)isEqual:(id)object {
    return [_target isEqual:object];
}

- (NSUInteger)hash {
    return [_target hash];
}

- (Class)superclass {
    return [_target superclass];
}

- (Class)class {
    return [_target class];
}

- (BOOL)isKindOfClass:(Class)aClass {
    return [_target isKindOfClass:aClass];
}

- (BOOL)isMemberOfClass:(Class)aClass {
    return [_target isMemberOfClass:aClass];
}

- (BOOL)conformsToProtocol:(Protocol *)aProtocol {
    return [_target conformsToProtocol:aProtocol];
}

- (BOOL)isProxy {
    return YES;
}

- (NSString *)description {
    return [_target description];
}

- (NSString *)debugDescription {
    return [_target debugDescription];
}
2.模拟多继承

Objective-C不支持多继承,但是使用NSProcy可以模拟多继承。苹果文档有如下示例模拟多继承

@interface TargetProxy : NSProxy {
    id realObject1;
    id realObject2;
}

- (id)initWithTarget1:(id)t1 target2:(id)t2;

@end

int main(int argc, const char *argv[]) {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    // Create an empty mutable string, which will be one of the
    // real objects for the proxy.
    NSMutableString *string = [[NSMutableString alloc] init];

    // Create an empty mutable array, which will be the other
    // real object for the proxy.
    NSMutableArray *array = [[NSMutableArray alloc] init];

    // Create a proxy to wrap the real objects.  This is rather
    // artificial for the purposes of this example -- you'd rarely
    // have a single proxy covering two objects.  But it is possible.
    id proxy = [[TargetProxy alloc] initWithTarget1:string target2:array];

    // Note that we can't use appendFormat:, because vararg methods
    // cannot be forwarded!
    [proxy appendString:@"This "];
    [proxy appendString:@"is "];
    [proxy addObject:string];
    [proxy appendString:@"a "];
    [proxy appendString:@"test!"];

    NSLog(@"count should be 1, it is: %d", [proxy count]);

    if ([[proxy objectAtIndex:0] isEqualToString:@"This is a test!"]) {
        NSLog(@"Appending successful.", proxy);
    } else {
        NSLog(@"Appending failed, got: '%@'", proxy);
    }

    NSLog(@"Example finished without errors.");
    [pool release];
    return 0;
}


@implementation TargetProxy

- (id)initWithTarget1:(id)t1 target2:(id)t2 {
    realObject1 = [t1 retain];
    realObject2 = [t2 retain];
    return self;
}

- (void)dealloc {
    [realObject1 release];
    [realObject2 release];
    [super dealloc];
}

// The compiler knows the types at the call site but unfortunately doesn't
// leave them around for us to use, so we must poke around and find the types
// so that the invocation can be initialized from the stack frame.

// Here, we ask the two real objects, realObject1 first, for their method
// signatures, since we'll be forwarding the message to one or the other
// of them in -forwardInvocation:.  If realObject1 returns a non-nil
// method signature, we use that, so in effect it has priority.
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *sig;
    sig = [realObject1 methodSignatureForSelector:aSelector];
    if (sig) return sig;
    sig = [realObject2 methodSignatureForSelector:aSelector];
    return sig;
}

// Invoke the invocation on whichever real object had a signature for it.
- (void)forwardInvocation:(NSInvocation *)invocation {
    id target = [realObject1 methodSignatureForSelector:[invocation selector]] ? realObject1 : realObject2;
    [invocation invokeWithTarget:target];
}

// Override some of NSProxy's implementations to forward them...
- (BOOL)respondsToSelector:(SEL)aSelector {
    if ([realObject1 respondsToSelector:aSelector]) return YES;
    if ([realObject2 respondsToSelector:aSelector]) return YES;
    return NO;
}

@end

http协议概览(http1.0/http1.1/http2)

http介绍

到目前为止,绝大多数web应用都是基于http来进行开的,我们对web的操作都是通过http协议进行数据传输。总结来说就是http协议是client 与 server通信的一种协议、标准或者格式。

http主要版本

1.http1.0
2.http1.1
3.http2.0

各版本间区别

首先说下http1.0 跟 http1.1,这俩时间线上最接近,但是差别也不小,http1.1基本是现在使用最广泛的http协议,也已经稳定运行了好多年。

http1.0 跟 http1.1区别

这两货之间我觉得最大的区别是1.1支持持久化链接(长链接),而1.0默认短连接即每次与服务器交互都需要新开一个新链接(如果需要开启长链接需加上keep-alive的请求首部)。
这就要人命了,试想一下:请求一张图片 或者 csss js等静态资源都需要新开一条链接,而http是基于tcp协议的,新开链接都得经过3次握手,四次挥手,现在一个网页光静态资源数几十上百个,如果使用1.0的话,那用户恐怕得等到骂街了,而且服务器资源有限,可以同时开的链接数有限。
1.1通过持久化链接解决了这个问题:一次链接,多次复用,但是如果阻塞了同样会新开链接(一个tcp链接支持的请求数有限)。
相对于持久化链接,http1.1还有几个比较重要的点
1.增加host字段
2.引入Chunked transfer-coding,范围请求,实现断点续传(实际上就是利用HTTP消息头使用分块传输编码,将实体主体分块传输)
3.HTTP 1.1管线化(pipelining)理论,客户端可以同时发出多个HTTP请求,而不用一个个等待响应之后再请求
*** 注意:这个pipelining仅仅是限于理论场景下,大部分桌面浏览器仍然会选择默认关闭HTTP pipelining!所以现在使用HTTP1.1协议的应用,都是有可能会开多个TCP连接的!***

下面开始介绍http2,首先我说下http2的基础知识

http1.1跟 http2区别

上面提到http1.1理论上支持管线化,但这个功能浏览器默认还是关闭的,所以基本没有实用性。下面我放张图,大家可以看下管线化跟非管线化之间有何区别

无论是HTTP1.0还是HTTP1.1提出了Pipelining理论,但还是会出现阻塞的情况。从专业的名词上说这种情况,叫做线头阻塞(Head of line blocking)简称:HOLB

下面来看看具体http2是如何解决这些问题的
http2 性能增强的核心:二进制分帧层, 它定义了如何封装http消息并在client 跟 server之间传输

与HTTP1.x的采用的换行符分隔文本不同,HTTP/2 消息被分成很小的消息和frame,然后每个消息和frame用二进制编码。客户端和服务端都采用二进制编码和解码。

流、消息、帧
:已经建立的连接之间双向流动的字节,它能携带一个至多个消息。
消息:一个完整的帧序列,它映射到逻辑的请求和响应消息。
:在HTTP/2通信的最小单元。每个桢包括一个帧头,里面有个很小标志,来区别是属于哪个流。
1.所有的通信都建立在一个TCP连接上,可以传递大量的双向流通的流。
2.每个流都有独一无二的标志和优先级。
3.每个消息都是逻辑上的请求和相应消息。由一个或者多个帧组成。
4.来自不同流的帧可以通过帧头的标志来关联和组装起来。

在这个的基础上,http2实现了真正意义上的多路复用:
在HTTP/1.x中,用户想要多个并行的请求来提高性能,但是这样必须得使用多个TCP连接.这样的操作是属于HTTP/1.x 发送模型的直接序列.它能保证在每次连接中在一个时间点只有一个响应被发送出去.更糟糕的是,它使得队头阻塞和重要TCP连接的低效使用.
在HTTP/2中,新的二进制帧层,解除了这个限制.使得所有的请求和响应多路复用.通过允许客户端和服务端把HTTP消息分解成独立的帧,交错传输,然后在另一端组装.交错的多个并行的请求或者响应,而不需要阻塞.这样就不用新开tcp链接,所有请求在一个链接上就能实现,淘汰没必要的潜在因素来降低页面载入的时间.提升可用网络容积的使用率.

还有几个点我觉得是http2提升比较重要的点
一、流的优先级
为了能方便流的传输顺序,HTTP/2.0提出,使每个流都有一个权重和依赖.
每个流的权重值在1~256之间
每个流可以详细给出对其他流的依赖
这样的话可以尽快把一些主要关键的资源响应给客户端,提高页面的响应速度。
二、服务端推送
服务器为单个客户端请求发送多个响应的能力。也就是说,除了对原始请求的响应之外,服务器还可以向客户端推送额外的资源,而不需要客户端明确请求每一个资源!这样可以提前有服务端控制哪些资源需要被提前加载,提高页面的响应速度
二、头部压缩
这个没太多可说的,主要就是可以节省不必要的流量开支

参考文档: HTTP/2 新特性总结(https://www.jianshu.com/p/67c541a421f9)

iOS 从AFNetworking中获取请求度量信息

事情起因是想更多的获取到iOS客户端访问网络情况的一些信息。
于是翻看iOS网络库文档, 主要是NSURLSession相关的,找到一个NSURLSessionTaskMetrics,里边记录了每次Http请求中的可能多个重定向中每一次的具体情况的时间度量信息(记录在NSURLSessionTaskTransactionMetrics), 包括请求开始时间、DNS解析开始时间、DNS解析结束时间、开始连接时间、响应开始时间等等具体的信息, 虽然还是没有例如连接的服务器ip等信息,但总归是多了不少信息。

查看头文件,系统是通过是通过NSURLSessionTaskDelegate 的如下方法给的NSURLSessionTaskMetrics的回调信息。

/*
 * Sent when complete statistics information has been collected for the task.
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

绝大部分iOS客户端不会直接用NSURLSession,而是会借助第三方的网络库(AFNetworking)的封装,来更简单的使用,伴鱼也不例外,所以能不能把这个统计信息拿出来就看AFNetworking给不给力了…

查看了项目中现在用的AFNetworking源码,AFURLSessionManager 实现了NSURLSessionTaskDelegate, 但是搜一下“didFinishCollectingMetrics”,并没有实现这个方法,也就是,没办法直接用AFNetworking的api来做这件事情了。

第一时间想到的是不是升级下AFNetworking就好了, 于是来到AFNetworking的Github主页,还是这个类AFURLSessionManager, 找最新的代码搜了下,搜到了下边这个方法:

- (void)URLSession:(NSURLSession *)session
              task:(NSURLSessionTask *)task
didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics
{
    AFURLSessionManagerTaskDelegate *delegate = [self delegateForTask:task];
    // Metrics may fire after URLSession:task:didCompleteWithError: is called, delegate may be nil
    if (delegate) {
        [delegate URLSession:session task:task didFinishCollectingMetrics:metrics];
    }

    if (self.taskDidFinishCollectingMetrics) {
        self.taskDidFinishCollectingMetrics(session, task, metrics);
    }
}

一阵欣喜,看来生个级就好了,pod update一下,开心的等了不少时间执行完了之后发现,本地的AFNetworking代码里边还是没有这个方法… 看了下pod执行的提示,原来AFNetworking已经是最新的3.2.1版了,再仔细看Github上是看的master代码,切换到tag 3.2.1, 果然没有了这个方法…3.2.1是好久之前发布的了,AFNetworking不知道啥时候才能再发布把现在master的代码放上去,肯定是等不及它发布的。

那就想办法把这个信息从AFNetworking了里拿出来吧,OC办法会比较多的,想了下AFURLSessionManager既然没有实现这个方法,我们帮它实现好了,利用OC Runtime方式,把实现加到这个类上应该就可以了,动手实现了下:

@implementation XCHttpClient

+ (void)load
{
    Class afHttpSessionManagerClass = AFHTTPSessionManager.class;
    Class clientClass = self;
    SEL selector = @selector(URLSession:task:didFinishCollectingMetrics:);
    Method method = class_getInstanceMethod(clientClass, selector);
    IMP imp = class_getMethodImplementation(clientClass, selector);
    BOOL success =  class_addMethod(afHttpSessionManagerClass, selector, imp, method_getTypeEncoding(method));
    NSLog(@"af ---> class success: %@", @(success));
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics
{
    NSLog(@"didFinishCollectingMetrics.... %@", metrics);
}

这里只是为了试验没有考虑代码结构合理性,正常应该单独搞一个类来处理这个逻辑。断点、运行, 成功了…拿到了时间信息。
(此处后来想到有更简单的方式,不用runtime,读者可以简单思考下…)

还有一个问题是,之前是在请求成功或失败,做了一些处理,然后记录的网络请求信息,如何把这个详细的时间信息交给到请求成功或者失败的block里边,从而跟之前的逻辑串起来?

很自然的想到,是不是在回调中把信息记录到NSURLSessionTask这个对象上,这样成功或者失败都从这个对象把信息取出来?

写代码尝试了一下(标准的利用关联对象生成新属性):

static char XCHttpClientMetrics;
@interface NSURLSessionTask (XCHttpClientMetrics)
@property (nonatomic, strong) NSURLSessionTaskMetrics *metrics;
@end
@implementation NSURLSessionTask
- (void)setMetrics:(NSURLSessionTaskMetrics *)metrics
{
    objc_setAssociatedObject(self, &XCHttpClientMetrics, metrics, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSURLSessionTaskMetrics *)metrics
{
   return objc_getAssociatedObject(self, &XCHttpClientMetrics);
}
@end

相应的回调方法修改下实现如下:

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics
{
    task.metrics = metrics;
}

运行,Crash !!!, 未识别的selector,仔细检查了代码,没有问题,打断点调试下:

(lldb) po [task respondsToSelector:@selector(setMetrics:)]
NO

(lldb) po task.class
__NSCFLocalDataTask

(lldb) po task.superclass
__NSCFLocalSessionTask

(lldb) po task.superclass.superclass
__NSCFURLSessionTask

很熟悉的苹果的做法,用一个私有类替换了NSURLSessionTask,所以确实不响应setMetrics这个selector。
怎么办?给这个私有类加一个属性吗? 理论上是可以做的,不过想想好累…
参考下AFNetworking的做法吧,AFNetworking应该也给task记录了一些信息,不然没办法从回调的方式转成success/fail回调的方式,翻看代码:

- (void)setDelegate:(AFURLSessionManagerTaskDelegate *)delegate
            forTask:(NSURLSessionTask *)task
{
    NSParameterAssert(task);
    NSParameterAssert(delegate);

    [self.lock lock];
    self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)] = delegate;//关键代码
    [self addNotificationObserverForTask:task];
    [self.lock unlock];
}

关键是每个task有一个整数标识(taskIdentifier)! 于是解决方案就很简单了,找个地方存一个map,用taskIdentifier作为key,具体的metrics信息作为value,然后在成功或者失败回调里根据key把信息取出来就可以了。

由于代码太简单,就不贴了…测试通过。能看到metrics信息里边对于ip直连的请求dns解析开始时间等信息都是null了,对于复用了连接的请求,connect时间也是null了。仔细分析metrics信息又是另外一个话题了,需要模拟下各种网络环境分析成功或失败请求的数据,就不在这里分析了。

注意: 根据文档Task对象并不是从一开始请求,到请求结束都是用的同一个Task对象,而是可能中间会换成另外一个对象,只是identifier应该是相同的,可以参考方法- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask
的文档,也可以试验一下。