分布式系统消息中间件——RabbitMQ的使用思考篇

一、何时创建队列

从前面的文章我们知道,RabbitMQ可以选择在生产者创建队列,也可以在消费者端创建队列,也可以提前创建好队列,而生产者消费者直接使用即可。

RabbitMQ的消息存储在队列中,交换器的使用并不真正耗费服务器的性能,而队列会。如在实际业务应用中,需要对所创建的队列的流量、内存占用及网卡占用有一个清晰的认知,预估其平均值和峰值,以便在固定硬件资源的情况下能够进行合理有效的分配。

按照RabbitMQ官方建议,生产者和消费者都应该尝试创建(这里指声明操作)队列。这虽然是一个很好的建议,但是在我看来这个时间上没有最好的方案,只有最适合的方案。我们往往需要结合业务、资源等方面在各种方案里面选择一个最适合我们的方案。

如果业务本身在架构设计之初己经充分地预估了队列的使用情况,完全可以在业务程序上线之前在服务器上创建好(比如通过页面管理、RabbitMQ命令或者更好的是从配置中心下发),这样业务程序也可以免去声明的过程,直接使用即可。预先创建好资源还有一个好处是,可以确保交换器和队列之间正确地绑定匹配。很多时候,由于人为因素、代码缺陷等,发送消息的交换器并没有绑定任何队列,那么消息将会丢失:或者交换器绑定了某个队列,但是发送消息时的路由键无法与现存的队列匹配,那么消息也会丢失。当然可以配合mandatory参数或者备份交换器(关于mandatory参数的使用详细可参考我的上一篇文章) 来提高程序的健壮性。与此同时,预估好队列的使用情况非常重要,如果在后期运行过程中超过预定的阈值,可以根据实际情况对当前集群进行扩容或者将相应的队列迁移到其他集群。迁移的过程也可以对业务程序完全透明。此种方法也更有利于开发和运维分工,便于相应资源的管理。如果集群资源充足,而即将使用的队列所占用的资源又在可控的范围之内,为了增加业务程序的灵活性,也完全可以在业务程序中声明队列。至于是使用预先分配创建资源的静态方式还是动态的创建方式,需要从业务逻辑本身、公司运维体系和公司硬件资源等方面考虑。

二、持久化及策略

作为一个内存中间件,在保证了速度的情况下,不可避免存在如内存数据库同样的问题,即丢失问题。持久化可以提高RabbitMQ 的可靠性,以防在异常情况(重启、关闭、宕机等)下的数据丢失。RabbitMQ的持久化分为三个部分:交换器的持久化、队列的持久化和消息的持久化。

  1. 交换器的持久化

交换器的持久化是通过在声明队列是将durable 参数置为true 实现的(该参数默认为false)。如果交换器不设置持久化,那么在RabbitMQ 服务重启之后,相关的交换器元数据会丢失,不过消息不会丢失,只是不能将消息发送到这个交换器中了。对一个长期使用的交换器来说,建议将其置为持久化的。

  1. 队列的持久化

队列的持久化是通过在声明队列时将durable 参数置为true 实现的(该参数默认为false),如果队列不设置持久化,那么在RabbitMQ 服务重启之后,相关队列的元数据会丢失,此时数据也会丢失。正所谓”皮之不存,毛将焉附”,队列都没有了,消息又能存在哪里呢?

  1. 消息的持久化

队列的持久化能保证其本身的元数据不会因异常情况而丢失,但是并不能保证内部所存储的消息不会丢失。要确保消息不会丢失,需要将其设置为持久化。通过将消息的投递模式(BasicProperties中的DeliveryMode属性)设置为2即可实现消息的持久化。

因此,消息如果要想在Rabbit重启、关闭、宕机时能够恢复,需要做到以下三点:

  • 把消息的投递模式设置为2
  • 发送到持久化的交换器
  • 到达持久化的队列

注意:RabbitMQ 确保持久化消息能从服务器重启中恢复的方式是将它们写入磁盘上的一个持久化日志文件中。当发布一条持久化消息到持久化交换器时,Rabbit会在日志提交到日志文件后才发送响应(开启生产者确认机制)。之后,如果消息到了非持久化队列,它会自动从日志文件中删除,并且无法在服务器重启后恢复。因此单单只设置队列持久化,重启之后消息会丢失;单单只设置消息的持久化,重启之后队列消失,继而消息也丢失。单单设置消息持久化而不设置队列的持久化是毫无意义的。当从持久化队列中消费了消息后(并且确认后),RabbitMQ会在持久化日志中把这条消息标记为等待垃圾收集。而在消费持久化消息之前,若RabbitMQ服务器重启,会自动重建交换器、队列以及绑定,重播持久化日志文件中的消息到合适的队列或者交换器上(取决于宕机时,消息处在路由的哪个环节)。

为了保障消息不会丢失,也许我们可以简单粗暴的将所有的消息标记为持久化,但这样我们会付出性能的代价。写入磁盘的速度比写入内存的速度慢得不只一点点。对于可靠性不是那么高的消息可以不采用持久化处理以提高整体的吞吐量。在选择是否要将消息持久化时,需要在可靠性和吐吞量之间做一个权衡。

将交换器、队列、消息都设置了持久化之后就能百分之百保证数据不丢失了吗?

  • 从消费者来说,如果在订阅消费队列时将noAck参数设置为true ,那么当消费者接收到相关消息之后,还没来得及处理就宕机了,这样也算数据丢失。
  • 在持久化的消息正确存入RabbitMQ 之后,还需要有一段时间(虽然很短,但是不可忽视〉才能存入磁盘之中。RabbitMQ 并不会为每条消息都进行同步存盘的处理,可能仅仅保存到操作系统缓存之中而不是物理磁盘之中。如果在这段时间内RabbitMQ 服务节点发生了岩机、重启等异常情况,消息保存还没来得及落盘,那么这些消息将会丢失。

关于第一个问题,可以通过消费者确认机制来解决。而第二个问题可以通过生产者确认机制来解决,也可以使用镜像队列机制(镜像队列机制,将在运维篇总结)。生产者确认消费者确认请往下看。

三、生产者确认

上文我们知道,在使用RabbitMQ的时候,可以通过消息持久化操作来解决因为服务器的异常崩溃而导致的消息丢失,除此之外,我们还会遇到一个问题,当消息的生产者将消息发送出去之后,消息到底有没有正确地到达服务器呢?如果不进行特殊配置,默认情况下发送消息的操作是不会返回任何信息给生产者的,也就是默认情况下生产者是不知道消息有没有正确地到达服务器。如果在消息到达服务器之前己经丢失,持久化操作也解决不了这个问题,因为消息根本没有到达服务器,何谈持久化?

RabbitMQ针对这个问题,提供了两种解决方式:

  • 通过事务机制实现:
  • 通过发送方确认(publisher confirm)机制实现。

3.1 RabbitMQ 事务机制

RabbitMQ 客户端中与事务机制相关的方法有三个:channel.TxSelect(用于将当前信道设置为事务模式);channel.TxCommit(用于提交事务),channel.TxRollback(用于回滚事务)。在通过channel.TxSelect方法开启事务之后,我们便可以发布消息给RabbitMQ了,如果事务提交成功,则消息一定到达了RabbitMQ 中,如果在事务提交执行之前由于RabbitMQ异常崩溃或者其他原因抛出异常,这个时候我们便可以将其捕获,进而通过执行channel.TxRollback方法来实现事务回滚。示例代码如下所示:

  channel.TxSelect();//将信道设置为事务模式
  try
  {
      //do something
      var message = Encoding.UTF8.GetBytes("TestMsg");
      channel.BasicPublish("normalExchange", "NormalRoutingKey", true, null, message);
      //do something
      channel.TxCommit();//提交事务
  }
  catch (Exception ex)
  {
      //log(ex);
      channel.TxRollback();
  }

事务确实能够解决消息发送方和RabbitMQ之间消息确认的问题,只有消息成功被RabbitMQ接收,事务才能提交成功,否则便可在捕获异常之后进行事务回滚,与此同时可以进行消息重发。但是使用事务同样会带来一些问题。

  • 会阻塞,发布者必须等待broker处理每个消息。
  • 事务是重量级的,每次提交都需要fsync(),需要耗费大量的时间
  • 事务非常耗性能,会降低RabbitMQ的消息吞吐量。

3.2 发送方确认机制

前面介绍了RabbitMQ可能会遇到的一个问题,即消息发送方(生产者〉并不知道消息是否真正地到达了RabbitMQ。随后了解到在AMQP协议层面提供了事务机制来解决这个问题,但是采用事务机制实现会严重降低RabbitMQ的消息吞吐量,这里就引入了一种轻量级的方式一发送方确认(publisher confirm)机制。生产者将信道设置成confirm确认)模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都会被指派一个唯一的ID( 从1开始),一旦消息被投递到所有匹配的队列之后,RabbitMQ就会发送一个确认(BasicAck) 给生产者(包含消息的唯一ID),这就使得生产者知晓消息已经正确到达了目的地了。如果消息和队列是可持久化的,那么确认消息会在消息写入磁盘之后发出。

RabbitMQ发送方确认机制

发送方确认模式,示例代码如下:

 //示例1--同步等待
 channel.ConfirmSelect();//开启确认模式
 var message = Encoding.UTF8.GetBytes("TestMsg");
 channel.ExchangeDeclare("normalExchange", "direct", true, false, null);
 channel.QueueDeclare("normalQueue", true, false, false, null);
 channel.QueueBind("normalQueue", "normalExchange", "NormalRoutingKey");
 channel.BasicPublish("normalExchange", "NormalRoutingKey", true, null, message);
 //var result=channel.WaitForConfirmsOrDie(Timeout); 
 //WaitForConfirmsOrDie 使用WaitForConfirmsOrDie 在Rabbit发送Nack命令或超时时会抛出一个异常
 var result = channel.WaitForConfirms();//等待该信道所有未确认的消息结果
 if(!result){
     //send message failed;
 }
 //示例2--异步通知
 channel.ConfirmSelect();//开启确认模式
 var message = Encoding.UTF8.GetBytes("TestMsg");
 channel.ExchangeDeclare("normalExchange", "direct", true, false, null);
 channel.QueueDeclare("normalQueue", true, false, false, null);
 channel.QueueBind("normalQueue", "normalExchange", "NormalRoutingKey");
 channel.BasicPublish("normalExchange", "NormalRoutingKey", true, null, message);
 channel.BasicAcks += (model, ea) =>
 {
     //消息被投递到所有匹配的队列之后,RabbitMQ就会发送一个确认(Basic.Ack)给生产者(包含消息的唯一ID)
     //ea.Multiple为True代表 ea.DeliveryTag编号之前的消息均已被确认。
    //do something;
 };
 channel.BasicNacks += (model, ea) =>
 {
     //如果RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条nack(BasicNack) 命令
    //do something;
 };

关于生产者确认机制同样会有一些问题,broker不能保证消息会被confirm,只知道将会进行confirm。这样如果broker与生产者之间的连接断开,导致生产者不能收到确认消息,可能会重复进行发布。总之,生产者确认模式给客户端提供了一种较为轻量级的方式,能够跟踪哪些消息被broker处理,哪些可能因为broker宕掉或者网络失败的情况而重新发布。

    注意:事务机制和publisher confirm机制两者是互斥的,不能共存。如果企图将已开启事务模式的信道再设置为publisher confmn模式, RabbitMQ会报错,或者如果企图将已开启publisher confirm模式的信道设置为事务模式, RabbitMQ也会报错。在性能上来看,而到底应该选择事务机制还是Confirm机制,则需要结合我们的业务场景。

四、消费者确认

为了保证消息从队列可靠地达到消费者,RabbitMQ提供了消息确认机制(message acknowledgement)。消费者在订阅队列时,可以指定noAck参数,当noAck等于false时,RabbitMQ会等待消费者显式地回复确认信号后才从内存(或者磁盘)中移去消息(实质上是先打上删除标记,之后再删除)。当noAck等于true时,RabbitMQ会自动把发送出去的消息置为确认,然后从内存(或者磁盘)中删除,而不管消费者是否真正地消费到了这些消息。

采用消息确认机制后,只要设置noAck参数为false,消费者就有足够的时间处理消息(任务),不用担心处理消息过程中消费者进程挂掉后消息丢失的问题,因为RabbitMQ会一直等待持有消息直到消费者显式调用BasicAck命令为止。

当noAck参数置为false,对于RabbitMQ服务端而言,队列中的消息分成了两个部分:一部分是等待投递给消费者的消息:一部分是己经投递给消费者,但是还没有收到消费者确认信号的消息。如果RabbitMQ 一直没有收到消费者的确认信号,并且消费此消息的消费者己经断开连接,则RabbitMQ会安排该消息重新进入队列,等待投递给下一个消费者,当然也有可能还是原来的那个消费者。

RabbitMQ不会为未确认的消息设置过期时间,它判断此消息是否需要重新投递给消费者的唯一依据是消费该消息的消费者连接是否己经断开,这么设计的原因是RabbitMQ 允许消费者消费一条消息的时间可以很久很久。

关于RabbitMQ消费者确认机制示例代码如下:

  //推模式
  EventingBasicConsumer consumer = new EventingBasicConsumer(channel);
  //定义消费者回调事件
  consumer.Received += (model, ea) =>
  {
      //do someting;
      //channel.BasicReject(ea.DeliveryTag, requeue: true);//拒绝
      //requeue参数为true会重新将这条消息存入队列,以便可以发送给下一个订阅的消费者
      channel.BasicAck(ea.DeliveryTag, multiple: false);//确认
      //若:multiple参数为true,则确认DeliverTag这个编号之前的消息
  };
  channel.BasicConsume(queue: "queueName",
                      noAck: false,
                     consumer: consumer);

  //拉模式
  BasicGetResult result = channel.BasicGet("queueName", noAck: false);
  //确认
  channel.BasicAck(result.DeliveryTag, multiple: false);

RabbitMQ 消费者确认

如上,消费者在消费消息的同时,Rabbit会同步给予消费者一个DeliveryTag,这个DeliveryTag就像我们数据库中的主键,消费者在消费完毕后拿着这个DeliveryTag去Rabbit确认或拒绝这个消息。

void BasicAck(ulong deliveryTag, bool multiple);

void BasicReject(ulong deliveryTag, bool requeue);

void BasicNack(ulong deliveryTag, bool multiple, bool requeue);
  • deliveryTag:可以看作消息的编号,它是一个64位的长整型值,最大值是9223372036854775807。
  • requeue:如果requeue 参数设置为true,则RabbitMQ会重新将这条消息存入队列,以便可以发送给下一个订阅的消费者;如果requeue 参数设置为false,则RabbitMQ立即会把消息从队列中移除,而不会把它发送给新的消费者。
  • BasicReject命令一次只能拒绝一条消息,如果想要批量拒绝消息,则可以使用Basic.Nack这个命令。
  • multiple:在BasicAck中,multiple 参数设置为true 则表示确认deliveryTag编号之前所有已被当前消费者确认的消息。在BasicNack中,multiple 参数设置为true 则表示拒绝deliveryTag 编号之前所有未被当前消费者确认的消息。

    说明:将channel.BasicReject 或者channel.BasicNack中的requeue设置为false ,可以启用”死信队列”的功能。(关于死信队列请看我的上一篇文章 https://www.cnblogs.com/hunternet/p/9697754.html)。

上述requeue,都会将消息重新存入队列发送给下一个消费者(也有可能是其它消费者)。关于requeue还有下面一种用法。可以选择是否补发给当前的consumer。

//补发消息 true退回到queue中 /false只补发给当前的consumer
channel.BasicRecover(true);

    注意:RabbitMQ仅仅通过Consumer的连接中断来确认该Message并没有被正确处理。也就是说,RabbitMQ给了Consumer足够长的时间来做数据处理。如果忘记了ack,那么后果很严重。当Consumer退出时,Message会重新分发。然后RabbitMQ会占用越来越多的内存,由于RabbitMQ会长时间运行,这个“内存泄漏”是致命的。

五、消息分发与顺序

5.1 消息分发

当RabbitMQ 队列拥有多个消费者时,队列收到的消息将以轮询(round-robin)的分发方式发送给消费者。每条消息只会发送给订阅列表里的一个消费者。这种方式非常适合扩展,而且它是专门为并发程序设计的。如果现在负载加重,那么只需要创建更多的消费者来消费处理消息即可。
很多时候轮询的分发机制也不是那么优雅。默认情况下,如果有n个消费者,那么RabbitMQ会将第m条消息分发给第m%n (取余的方式)个消费者, RabbitMQ 不管消费者是否消费并己经确认了消息。试想一下,如果某些消费者任务繁重,来不及消费那么多的消息,而某些其他消费者由于某些原因(比如业务逻辑简单、机器性能卓越等)很快地处理完了所分配到的消息,进而进程空闲,这样就会造成整体应用吞吐量的下降。那么该如何处理这种情况呢?这里就要用到channel.BasicQos(int prefetchCount)这个方法,channel.BasicQos方法允许限制信道上的消费者所能保持的最大未确认消息的数量。
举例说明,在订阅消费队列之前,消费端程序调用了channel.BasicQos(5),之后订阅了某个队列进行消费。RabbitMQ 会保存一个消费者的列表,每发送一条消息都会为对应的消费者计数,如果达到了所设定的上限,那么RabbitMQ 就不会向这个消费者再发送任何消息。直到消费者确认了某条消息之后, RabbitMQ 将相应的计数减1,之后消费者可以继续接收消息,直到再次到达计数上限。

注意:Basic.Qos 的使用对于拉模式的消费方式无效.

void BasicQos(uint prefetchSize, ushort prefetchCount, bool global);
  • prefetchCount:允许限制信道上的消费者所能保持的最大未确认消息的数量,设置为0表示没有上限。
  • prefetchSize:消费者所能接收未确认消息的总体大小的上限,单位为B,设置为0表示没有上限。
  • global:对于一个信道来说,它可以同时消费多个队列,当设置了prefetchCount 大于0 时,这个信道需要和各个队列协调以确保发送的消息都没有超过所限定的prefetchCount 的值,这样会使RabbitMQ 的性能降低,尤其是这些队列分散在集群中的多个Broker节点之中。RabbitMQ 为了提升相关的性能,在AMQPO-9-1 协议之上重新定义了global这个参数。如下表所示:
global参数 AMQP 0-9-1 RabbitMQ
false 信道上所有的消费者都需要遵从prefetchCount 的限信道上新的消费者需要遵从prefetchCount 的限定值 信道上新的消费者需要遵从prefetchCount 的限定值
true 当前通信链路( Connection) 上所有的消费者都需信道上所有的消费者都需要遵从prefetchCount的限定值 信道上所有的消费者需要遵从prefetchCount 的限定值

注意:

  1. 对于同一个信道上的多个消费者而言,如果设置了prefetchCount 的值,那么都会生效。
//伪代码
Consumer consumer1 = ...;
Consumer consumer2 = ...;
channel.BasicQos(10) ; 
channel.BasicConsume("my-queue1" , false , consumer1);
channel.BasicConsume("my-queue2" , false , consumer2);
//两个消费者各自的能接收到的未确认消息的上限都为10 。
  1. 如果在订阅消息之前,既设置了global 为true 的限制,又设置了global为false的限制,RabbitMQ 会确保两者都会生效。但会增加RabbitMQ的负载因为RabbitMQ 需要更多的资源来协调完成这些限制。
//伪代码
Channel channel = ...;
Consumer consumerl = ...;
Consumer consumer2 = ...;
channel.BasicQos(3 , false); 
channel.BasicQos(5 , true); 
channel.BasicConsume("queuel" , false , consumerl) ;
channel.BasicConsume("queue2" , false , consumer2) ;
//这里每个消费者最多只能收到3个未确认的消息,两个消费者能收到的未确认的消息个数之和的上限为5

5.2 消息顺序

消息的顺序性是指消费者消费到的消息和发送者发布的消息的顺序是一致的。举个例子,不考虑消息重复的情况,如果生产者发布的消息分别为msgl、msg2、msg3,那么消费者必然也是按照msgl、msg2、msg3的顺序进行消费的。
目前很多资料显示RabbitMQ的消息能够保障顺序性,这是不正确的,或者说这个观点有很大的局限性。在不使用任何RabbitMQ的高级特性,也没有消息丢失、网络故障之类异常的情况发生,并且只有一个消费者的情况下,最好也只有一个生产者的情况下可以保证消息的顺序性。如果有多个生产者同时发送消息,无法确定消息到达Broker 的前后顺序,也就无法验证消息的顺序性。
那么哪些情况下RabbitMQ 的消息顺序性会被打破呢?下面介绍几种常见的情形。

  • 如果生产者使用了事务机制,在发送消息之后遇到异常进行了事务回滚,那么需要重新补偿发送这条消息,如果补偿发送是在另一个线程实现的,那么消息在生产者这个源头就出现了错序。同样,如果启用publisher confirm时,在发生超时、中断,又或者是收到RabbitMQ的BasicNack命令时,那么同样需要补偿发送,结果与事务机制一样会错序。或者这种说法有些牵强,我们可以固执地认为消息的顺序性保障是从存入队列之后开始的,而不是在发迭的时候开始的。
  • 考虑另一种情形,如果生产者发送的消息设置了不同的超时时间,井且也设置了死信队列,整体上来说相当于一个延迟队列,那么消费者在消费这个延迟队列的时候,消息的顺序必然不会和生产者发送消息的顺序一致。
  • 如果消息设置了优先级,那么消费者消费到的消息也必然不是顺序性的。
  • 如果一个队列按照前后顺序分有msg1, msg2、msg3、msg4这4 个消息,同时有ConsumerA和ConsumerB 这两个消费者同时订阅了这个队列。队列中的消息轮询分发到各个消费者之中,ConsumerA 中的消息为msg1和msg3,ConsumerB中的消息为msg2、msg4。ConsumerA收到消息msg1之后并不想处理而调用了BasicNack/BasicReject将消息拒绝,与此同时将requeue设置为true,这样这条消息就可以重新存入队列中。消息msg1之后被发送到了ConsumerB中,此时ConsumerB己经消费了msg2、msg4,之后再消费msg1.这样消息顺序性也就错乱了。

包括但不仅限于以上几种情形会使RabbitMQ 消息错序。如果要保证消息的顺序性,需要业务方使用的时候做进一步的处理。如在消息体内添加全局有序标识等。

六、消息传输保障

消息可靠传输一般是业务系统接入消息中间件时首要考虑的问题,一般消息中间件的消息
传输保障分为三个层级。

  • At most once: 最多一次。消息可能会丢失,但绝不会重复传输。
  • At least once: 最少一次。消息绝不会丢失,但可能会重复传输。
  • Exactly once: 恰好一次。每条消息肯定会被传输一次且仅传输一次。

RabbitMQ 支持其中的”最多一次”和”最少一次”。其中”最少一次”投递实现需要考虑以下这个几个方面的内容:

  1. 消息生产者需要开启事务机制或者publisher confirm 机制,以确保消息可以可靠地传
    输到RabbitMQ 中。
  2. 消息生产者需要配合使用mandatory参数或者备份交换器来确保消息能够从交换器
    路由到队列中,进而能够保存下来而不会被丢弃。
  3. 消息和队列都需要进行持久化处理,以确保RabbitMQ服务器在遇到异常情况时不会造成消息丢失。
  4. 消费者在消费消息的同时需要将noAck设置为false,然后通过手动确认的方式去确认己经正确消费的消息,以避免在消费端引起不必要的消息丢失。

“最多一次”的方式就无须考虑以上那些方面,生产者随意发送,消费者随意消费,不过这样很难确保消息不会重复消费。

“恰好一次”是RabbitMQ目前无法保障的(目前我也不知道哪个中间件能够保证)。消费者在消费完一条消息之后向RabbitMQ 发送确认BasicAck命令,此时由于网络断开或者其他原因造成RabbitMQ并没有收到这个确认命令,那么RabbitMQ不会将此条消息标记删除。在重新建立连接之后,消费者还是会消费到这一条消息,这就造成了重复消费。再考虑一种情况,生产者在使用publisher confirm机制的时候,发送完一条消息等待RabbitMQ 返回确认通知,此时网络断开,生产者捕获到异常情况,为了确保消息可靠性选择重新发送,这样RabbitMQ中就有两条同样的消息,在消费的时候,消费者就会重复消费。而解决重复消费可以通过消费者幂等等方式来解决。

结束语

本篇文章,我们思考了使用RabbitMQ过程中需要注意的几个问题,而前两篇文章对RabbitMQ的概念以及如何使用做了简单的介绍,相信经过这些介绍已经对RabbitMQ有了基本的了解。但这些远远不够,想要更好的利用好RabbitMQ还需要结合我们的业务场景来更多的去使用它(切记不要为了使用技术而使用技术!)。关于RabbitMQ的运维篇,会在以后的文章中继续给大家分享。

算法相关的基础概念

什么是算法

通过上一篇对图灵机原理的讲解,我们知道,一个计算问题描述的是输入/输出之间的关系,如果根据给定的输入能设计一个程序计算出期望的输出,就认为这个问题可解。这个程序的计算过程就是用算法来描述的,通过算法这个工具我们就容易设计出这样的一个程序。

确切地说,算法是有限步骤的计算过程,该过程取某个值或集合作为输入,并产生某个值或集合作为输出。

算法的正确与错误

如果算法对于每个输入都可以正确的停机,则称该算法是正确的,并称正确的算法解决了给定的计算问题。一个不正确的算法对于某个输入可能根本不停机,也可能以不正确的结果停机,比如一个排序问题:

输入:一个长度为n的数组(a1,a2,a3…)

输出:输入数组排序后的一个数组(b1,b2,b3….),满足:b1≤b2≤b3

如果根据这个问题设计出来的算法交给图灵机运行输出的结果与人们的期望相反(比如输出:b3,b2,b1…),那么这个算法就是错误的。

随机访问机模型

针对同一计算问题,可以设计出多种算法,其中有好有坏,我们可以通过预测算法需要的资源来筛选出好的算法。虽然有时我们关心内存、带宽这类硬件资源,但通常我们度量的是运行时间。

度量算法的运行时间,人们通常用的是随机访问机模型(Random-Access Machine, RAM)。在 RAM 模型中:

  • 指令一条接着一条执行的,没有并发操作。
  • 指令包含了真实计算机的常见指令:算数指令(加法,减法,乘法,除法,取余等)、数据移入指令(装入,存储,复制)和控制命令(条件与无条件转移、子程序调用与返回);
  • 每条指令所用的时间均为常量。

RAM 模型假定的观点是,运行每行伪代码所需的时间是一个常量时间,虽然真实计算机执行一行代码与另一行代码需要不同的常量时间。依此,一个算法在特定输入上的运行时间不是指现实意义的时间,而是执行指令的次数。

比如下面这段 foo 函数的代码:

function foo(n) {
  for (let i = 0; i < n; i++) {   // 2n+1 次
    console.log('Hello, World!')  // n 次
  }
  return 0                        // 1 次
}

上面的代码需要执行 2n + 1 + n + 1 = 3n + 2 次指令,也就是说执行时间是 3n + 2,如果用一个时间函数来表示,就是 T(n) = 3n + 2。

算法的时间复杂度

根据 RAM 模型,一个算法可以在给定的输入规模 n 下分析出一个运行时间的函数 T(n)。研究 T(n) 常用的一种策略是分析输入规模 n 增大的情况下 T(n) 的变化(如线性增长、指数增长等)。如果用 f(n) 来表示 T(n) 的增长速度,那么 f(n) 和 T(n) 的关系我们约定用一个大O来表示,即:

T(n) = O(f(n))

这就是大O表示法。由于输入规模 n 的增长率与 f(n) 的增长率是正相关的,所以称作渐近时间复杂度(Asymptotic Time Complexity),简称时间复杂度。相对应的,还有空间复杂度,这里我们不作讨论。

当 n 足够大时或趋于无穷大时,T(n) 的常数部分就变得不重要,我们真正关心的是运行时间的增长量级或增长率。如果用 f(n) 来表示增长数度,上面 foo 示例代码的增长速度可以表示为 f(n) = n,把它代入到 T(n) = O(f(n)) 就是:

T(n) = O(n)

这时,我们称 foo 的时间复杂度为 O(n)。

常见的时间复杂度有:

  • 常数阶 O(1),
  • 对数阶 O(log2^n),
  • 线性阶 O(n),
  • 线性对数阶 O(nlog2^n),
  • 平方阶 O(n^2),
  • 立方阶 O(n^3),
  • k 次方阶 O(n^k),
  • 指数阶 O(2^n)。

随着问题规模 n 的不断增大,上述时间复杂度不断增大,算法的执行效率也越低。大O表示法只是一种估算,当输入规模足够大的时候才有意义。

注意,大O表示法考虑的是最坏的情况。比如,从一个长度为 n 的数组中找一个值等于 10 的元素,开始遍历扫描这个数组,有可能第 1 次就扫到了,也有可能是第 n 次才扫到。这里最坏的情况是 n 次,所以时间复杂度就是 O(n)。

大部分情况下你用直觉就可以知道一个算法的大O表示。比如,如果用一个循环遍历输入的每个元素,那么这个算法就是 O(n);如果是用循环套循环,那就是 O(n^2),以此类推。

深入理解Git的实现原理

0、导读
本文适合对git有过接触,但知其然不知其所以然的小伙伴,也适合想要学习git的初学者,通过这篇文章,能让大家对git有豁然开朗的感觉。在写作过程中,我力求通俗易懂,深入浅出,不堆砌概念。你能够从本文中了解以下知识:
  • Git是什么
  • Git能够解决哪些问题
  • Git的实现原理

请注意,本文的阐述逻辑是:Git是什么——>Git要解决的根本问题是什么——>git是如何解决这些问题的。

1、Git是什么?
Git是一种分布式版本控制系统。
有人要问了,什么是“版本控制”?Git又为什么被冠以“分布式”的名头呢?这两个问题我们一一解答。
版本控制这个说法多少有一点抽象。事实上,版本控制这件事儿我们一直在做,只是平时不这么称呼。举一个栗子,boss让你写一个策划案,你先完成了一稿,之后又有了一些新的想法,但是并不确定新的想法是否能得到boss的认可,于是你保存了一个初稿,之后在初稿的基础上另存了一个文件,做了部分修改完成了一个修改稿。OK,这时你的策划案就有了两个版本——初稿和修改稿。如果boss对修改稿不满意,你可以很轻易的把初稿拿出来交差。
在这个简单的过程中,你已经执行了一个简单的版本控制操作——把文档保存为初稿和修改稿的过程就是版本控制。
学术点说,版本控制就是对文件变更过程的管理。说白了,版本控制就是要把一个文件或一些文件的各个版本按一定的方式管理起来,目的是需要用到某个版本的时候可以随时拿出来。

另一个个问题,为什么说Git是“分布式”版本控制系统呢?

这里的“分布式”是相对于“集中式”来说的。把数据集中保存在服务器节点,所有的客户节点都从服务节点获取数据的版本控制系统叫做集中式版本控制系统,比如svn就是典型的集中式版本控制系统。

与之相对,Git的数据不止保存在服务器上,同时也完整的保存在本地计算机上,所以我们称Git为分布式版本控制系统。

Git的这种特性带来许多便利,比如你可以在完全离线的情况下使用Git,随时随地提交项目更新,而且你不必为单点故障过分担心,即使服务器宕机或数据损毁,也可以用任何一个节点上的数据恢复项目,因为每一个开发节点都保存着完整的项目文件镜像。

 

2、Git能够解决哪些问题?
就像上文举的例子一样,在未接触版本控制系统之前,大多人会通过保存项目或文件的备份来达到版本控制的目的。通常你的文件或文件夹名会设置成“XXX-v1.0”、“XXX-v2.0”等。
这是一种简单的办法,但过于简单。这种方式无法详细记录版本附加信息,难以应付复杂项目或长期更新的项目,缺乏版本控制约定,对协作开发无能为力。如果你不慎使用了这种方式,那么稍稍过一段时间你就会发现连自己都不知道每个版本间的区别,版本控制形同虚设。
Git能够为我们解决版本控制方面的大多数问题,利用Git
  • 我们可以为每一次变更提交版本更新并且备注更新的内容;
  • 我们可以在项目的各个历史版本之间自如切换;
  • 我们可以一目了然的比较出两个版本之间的差异;
  • 我们可以从当前的修改中撤销一些操作;
  • 我们可以自如的创建分支、合并分支;
  • 我们可以和多人协作开发;
  • 我们可以采取自由多样的开发模式。
诸如此类,数不胜数。然而实现这些功能的基础是对文件变更过程的存储。如果我们能抓住这个根本,提纲挈领的学习git,会事半功倍。
随着对Git更深入的学习,你会发现它会变得越来越简单,越来越纯粹。道家有万法归宗的说法,用在这里再合适不过。因为Git之所以有如此多炫酷的功能,根源只有一个:它很好的解决了文件变更过程存储这一个问题。

所以,如果问“Git能够解决哪些问题?”我们可以简单的回答:Git解决了版本控制方面的很多问题,但最核心的是它很好的解决了版本状态存储(即文件变更过程存储)的问题。

3、Git的实现原理
我们说到,Git很好的解决了版本状态记录的问题,在此基础上实现了版本切换、差异比较、分支管理、分布式协作等等炫酷功能。那么,这一节我们就先从最根本的讲起,看看Git是如何解决版本状态记录(即文件变更过程记录)问题的。
我们都有版本记录的经验,比如在文档撰写的关键点上保留一个备份,或在需要对文件进行修改的时候“另存”一次。这都是很好的习惯,也是版本状态记录的一种常用方式。事实上,Git采取了差不多的方式。
在我们向Git系统提交一个版本的时候,Git会把这个版本完整保存下来。这是不是和“另存”有异曲同工之妙呢?不同之处在于存储方式,在Git系统中一旦一个版本被提交,那么它就会被保存在“Git数据库”中。
3.1 Git数据库
我们提到了“Git数据库”,这是什么玩意儿呢?为了能够说清楚Git数据库的概念,我们暂且引入三个Git指令,通过这三个命令,我们就能一探git数据库的究竟。
  • git init  用于创建一个空的git仓库,或重置一个已存在的git仓库
  • git hash-object  git底层命令,用于向Git数据库中写入数据
  • git cat-file  git底层命令,用于查看Git数据库中数据
首先,我们用git init新建一个空的git仓库。(希望小伙伴们可以跟着我的节奏一起来实际操作一下,会加深理解。如果有还没有安装好git工具的同学,请自行百度安装,我不会讲安装的过程。)
我用的是ubuntu系统,在terminal下执行
$ git init GitTest
这一命令在当前目录下生成了一个新的文件夹-GitTest,在GitTest中,包含了一个新建的空git仓库。如果你不明白git仓库是什么,那么可以简单的理解为存放git数据的一个空间,这这个例子中,是“/home/mp/Workspace/GitTest/.git”目录。
接下来,我们看看git仓库的结构是什么样的。执行
$ cd GitTest
$ ls
$ find .git
执行结果如图所示,我们发现,GitTest目录下,除隐藏目录.git之外,并没有其他文件或文件夹。
我们通过find .git命令查看新生成的空git仓库的结构,会发现其中有一个objects文件夹,这就是git数据库的存储位置
3.1 Git数据库的写入操作
紧接着,我们利用git底层命令git hash-object向git数据库中写入一些内容。执行命令:
$ echo “version 1” | git hash-object -w –stdin
这是一条通道命令,意思是把“|”前边的命令的输出作为“|”后边命令的输入。git hash-object -w –stdin 的意思是向git数据库中写入一条数据(-w),这条数据的内容从标准输入中读取(–stdin)。
命令执行结果如上图。命令执行后,会返回个长度为40位的hash值,这个hash值是将待存储的数据外加一个头部信息一起做SHA-1校验运算而得的校验和。在git数据库中,它有一个名字,叫做“键值(key)”。相应的,git数据库其实是一个简单的“键值对(key-value)”数据库。事实上,你向该数据库中插入任意类型的内容,它都会返回一个键值。通过返回的键值可以在任意时刻再次检索该内容。
此时,我们再次执行find .git/objects/ -type f命令查看objects目录,会发现目录中多出了一个文件,这个文件存储在以新存入数据对应hash值的前2位命名的文件夹内,文件名为hash值的后38位。这就是git数据库的存储方式,一个文件对应一条内容,就是这么简单直接。
3.2 Git数据库的查询操作
我们可以通过git cat-file这个git底层命令查看数据库中某一键值对应的数据。执行
$ git cat-file -t  83baa
$ git cat-file -p 83baa
其中,-t选项用于查看键值对应数据的类型,-p选项用于查看键值对应的数据内容,83bba为数据键值的简写。
执行结果如上图。可见,所查询的键值对应的数据类型为blob,数据内容为“version 1”。blob对象我们称之为数据对象,这是git数据库能够存储的对象类型之一,后面我们还会讲到另外两种对象分别是树(tree)对象和提交(commit)对象。
截止到这里,你已经掌握了如何向git数据库里存入内容和取出内容。这很简单但是却意义非凡,因为对git数据库的操作正是git系统的核心——git的版本控制功能就是基于它的对象数据库实现的。在git数据库里,存储着纳入git版本管理的所有文件的所有版本的完整镜像。
git这么简单吗?不用怀疑,git就是这么简单,我们已经准确的抓住了它的根本要义——对象数据库。接下来我们会利用git数据库搭建起git的高楼大厦。
3.3 使用Git跟踪文件变更
我们明白,所谓跟踪文件变更,只不过是把文件变更过程中的各个状态完整记录下来。
我们模拟一次文件变更的过程,看看仅仅利用git的对象数据库能不能实现“跟踪文件变更”的功能。
首先,我们执行
$ echo “version 1” > file.txt
$ git hash-object -w file.txt
我们把文本“version 1”写入file.txt中,并利用git hash-object -w file.txt命令将其保存入数据库中。如果你足够细心,会发现返回的hash键值和利用echo “version 1” | git hash-object -w –stdin写入数据库时是一致的,这很正常,因为我们写入的内容相同。git hash-object命令在只指定-w选项的时候,会把file.txt文件内容写入数据库。

此时,执行
$ find .git/objects -type f

会发现.git/objects目录中,依然只有一个文件。可见,git数据库存储文件时,只关心文件内容,与文件的名字无关。

接下来,我们修改file.txt的内容,执行
$ echo “version 2” > file.txt
$ git hash-object -w file.txt
$ find .git/objects

执行结果如上图。我们发现,.git/objects下多出了一个文件,这是我们新保存进数据库的file.txt。接下来,我们执行git cat-file搞清楚这两条数据的内容分别是什么。执行
$git cat-file -p 83baa
$git cat-file -p 1f7a7a

执行结果如上图,file.txt的变更过程被完整的记录下来了。
如果我们想把文件恢复到修改为“version 2”之前的状态,只需执行
$ git cat-file -p 83baa > file.txt

执行结果如上图。file.txt成功的恢复到了修改前的状态。

文件变更状态跟踪的道理就是这么简单。但做到这一步还远远不算完美,至少有以下几方面的问题:

  • 第一,无法记录文件名的变化;
  • 第二,无法记录文件夹的变化;
  • 第三,记忆每一个版本对应的hash值无聊且乏味且不可能;
  • 第四,无法得知文件的变更时序;
  • 第五,缺少对每一次版本变化的说明。
问题不少,但都是简单的小问题,我们一一解决。
3.4 利用树对象(tree object)解决文件名保存和文件组织问题
Git利用树对象(tree object)解决文件名保存的问题,树对象也能够将多个文件组织在一起。

Git通过树(tree)对象将数据(blob)对象组织起来,这很类似于一种文件系统——blob对象对应文件内容,tree对象对应文件的目录和节点。一个树(tree)对象包含一条或多条记录,每条记录含有一个指向blob对象或tree对象的SHA-1指针,以及相应的模式、类型、文件名。
有了树对象,我们就可以将文件系统任何时间点的状态保存在git数据库中,这是不是很激动人心呢?你的一个复杂的项目可能包含成百上千个文件和文件目录,有了树对象,这一切都不是问题。
创建树对象
 
通常,Git根据某一时刻暂存区所表示的状态创建并记录一个对应的树对象,如此重复便可以依次记录一系列的树对象。Git的暂存区是一个文件——.git/index。下面,我们通过创建树对象的过程来认识暂存区和树对象。
为了创建一个树对象,我们需要通过暂存一些文件来创建一个暂存区。为此我们引入两个命令:
  • git update-index     git底层命令,用于创建暂存区
  • git ls-files –stage    git底层命令,用于查看暂存区内容
  • git write-tree            git底层命令,用于将暂存区内容写入一个树对象

OK,万事俱备,我们将file.txt的第一个版本放入暂存区,执行
$ cat .git/index
$ git update-index –add  file.txt #-add选项必须指定,因为该文件并不在缓冲区中。
$ cat .git/index
$ find .git/objects -type f
$ git ls-files –stage
$ git write-tree
$ find .git/objects -type f
执行结果如上图。
首先,我们注意.git/index文件的变化。在添加file.txt到暂存区前,index文件并不存在,这说明暂存区还没有创建。添加file.txt到暂存区的同时,index文件被创建。
其次,我们看git数据库的变化。我们发现在执行git update-index 之后,git数据库并没有改变,依然是只有两条数据。在执行git write-tree之后,git数据库中多出了一条新的记录。
我们执行git cat-file来查看一下多出来的这条记录是什么内容。执行
$ git cat-file -t 16b06f
$ git cat-file -p 16b06f
执行结果如上图。可见,git数据库中新增加的记录是一个tree对象,该tree对象指向一个blob对象,hash键值为
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
这一个blob对象是之前我们添加进数据库的。

以上我们添加了一个已经存在在git数据库中的文件到暂存区,如果我们新建一个未曾保存到git数据库的文件存入暂存区,进而保存为tree对象,会有什么不同吗?我们试试看。执行
$ echo “new file” > new
$ git update-index –add new
$ find .git/objects -type f
$ git write-tree
$ find .git/objects -type f
执行结果如上图。我们可以看到,这一次执行update-index之后,和上次不同,git数据库发生了变化,新增加了一条hash键值为“fa49b0”的数据;暂存区中也多出了文件new的信息。

这说明两个问题:
  • 如果添加git数据库中尚未存储的数据到暂存区,则在执行update-index的时候,会同时把该数据保存到git数据库。
  • 添加文件进入暂存区的操作是追加操作,之前已经加入暂存区的文件依然存在。
此时,我们查看新添加的树对象,执行

$ git cat-file -p 34bd3b

执行结果如上图。此次write-tree写入数据库的是tree对象包含了两个文件。
更进一步,我们是否能将一个子文件夹保存到树对象呢?尝试一下,执行
$ mkdir new_dir
$ git update-index –add new_dir
执行结果如上图。我们发现,无法将一个新建的空文件夹添加到暂存区,错误提示告诉我们,应该将文件分别加入到暂存区。

OK,接下来,我们在新建的文件夹下写入一个文件,再尝试将这一文件加入暂存区。执行
$ echo “file in new dir” > new_dir/new
$ git update-index –add new_dir/new
$ git ls-files –stage

执行结果如上图。文件夹new_dir对应一个tree对象。

至此,在git数据库中,我们可以完整的记录文件的状态、文件夹的状态;并且可以把多个文件或文件夹组织在一起,记录他们的变更过程。这是不是很炫酷,我们离一个完善的版本控制系统似乎已经不远了,而这一切实现起来又是如此简单——我们只是通过几个命令操作git数据库就完成了这些功能。
接下来,我们只要把数据库中各个版本的时序关系记录下来,再把对每一个版本更新的注释记录下来,不就完成了一个逻辑简单、功能强大、操作灵活的版本控制系统吗?
那么,如何记录版本的时序关系,如何记录版本的更新注释呢?这就要引入另一个git数据对象——提交对象(commit object)。
3.5 利用提交对象(commit object)记录版本间的时序关系和版本注释
 
commit对象能够帮你记录什么时间,由什么人,因为什么原因提交了一个新的版本,这个新的版本的父版本又是谁。
git提供了底层命令commit-tree来创建提交对象(commit object),我们需要为这个命令指定一个被提交的树对象的hash键值,以及该提交对象的父提交对象(如果是第一次提交,不需要指定父对象)。
我们尝试将之前创建的树对象提交为commit 对象,执行
$ git write-tree
cb0fbcc484a3376b3e70958a05be0299e57ab495
$ git commit-tree cb0fbcc -m “first commit”
7020a97c0e792f340e00e1bb8edcbafcc4dfb60f
$ git cat-file 7020a97
tree cb0fbcc484a3376b3e70958a05be0299e57ab495
author john <john@163.com> 1537961478 +0800
committer john <john@163.com> 1537961478 +0800

first commit
在git commit-tree命令中,-m选项用于指定本次提交的注释。
我们可以很清楚的看到,一个提交对象包含着所提交版本的树对象hash键值,author和commiter,以及修改和提交的时间,最后是本次提交的注释。
其中committer和author是通过git config命令设置的。
接下来,修改某个文件,重新创建一个树对象,并将这一树对象提交,作为项目的第二个提交版本。执行
$ echo “new version” > file.txt
$ git update-index file.txt
$ git write-tree
848e967643b947124acacc3a2d6c5a13c549231c
$ git commit-tree 848e96 -p 7020a97 -m “second commit”
e838c8678ef789df84c2666495663060c90975d7
$ git cat-file -p e838c
tree 848e967643b947124acacc3a2d6c5a13c549231c
parent 7020a97c0e792f340e00e1bb8edcbafcc4dfb60f
author john <john@163.com> 1537962442 +0800
committer john <john@163.com> 1537962442 +0800

second commit
我们可以按照上述步骤,再提交第三个版本。
$ echo “another version” > file.txt
$ git update-index file.txt
$ git write-tree
92867fcc5e0f78c195c43d1de25aa78974fa8103
$ git commit-tree 92867 -p e838c -m “third commit”
491404fa6e6f95eb14683c3c06d10ddc5f8e883f
$ git cat-file -p 49140
tree 92867fcc5e0f78c195c43d1de25aa78974fa8103
parent e838c8678ef789df84c2666495663060c90975d7
author john <john@163.com> 1537963274 +0800
committer john <john@163.com> 1537963274 +0800 

third commit
提交完三个版本,我们通过git log 查看最近一个提交对象的提交记录
$ git log 49140
commit 491404fa6e6f95eb14683c3c06d10ddc5f8e883f
Author: john <john@163.com>
Date:   Wed Sep 26 20:01:14 2018 +0800

    third commit

commit e838c8678ef789df84c2666495663060c90975d7
Author: john <john@163.com>
Date:   Wed Sep 26 19:47:22 2018 +0800

    second commit

commit 7020a97c0e792f340e00e1bb8edcbafcc4dfb60f
Author: john <john@163.com>
Date:   Wed Sep 26 19:31:18 2018 +0800

    first commit

太神奇了: 就在刚才,我们围绕git数据库,仅凭几个底层数据库操作便完成了一个 Git 提交历史的创建。到此为止,我们已经完全掌握了git的内在逻辑。
接触过git的小伙伴会发现,以上我们用到的这些指令在使用git过程中是用不到的。这是为什么呢?因为git对以上这些指令进行了封装,给用户提供了更便捷的操作命令,如add,commit等。
每次我们运行 git add 和 git commit 命令时, Git 所做的实质工作是将被改写的文件保存为数据对象,更新暂存区,记录树对象,最后创建一个指明了顶层树对象和父提交的提交对象。 这三种主要的 Git 对象——数据对象、树对象、提交对象——最初均以单独文件的形式保存在 .git/objects 目录下。
 
然而,小问题依然存在,截止目前为止,我们对版本和数据对象的操作都是基于hash键值的,这些毫无直观含义的字符串让人很头疼,不会有人愿意一直急着最新提交对应的hash键值的。git不会允许这样的问题存在的,它通过引入“引用(references)”来解决这一问题。
 
3.6 Git的引用
 
Git的引用(references)保存在.git/refs目录下。git的引用类似于一个指针,它指向的是某一个hash键值。
创建一个引用实在再简单不过。我们只需把一个git对象的hash键值保存在以引用的名字命名的文件中即可。
执行
$ echo “491404fa6e6f95eb14683c3c06d10ddc5f8e883f” > .git/refs/heads/master
$ cat .git/refs/heads/master 
491404fa6e6f95eb14683c3c06d10ddc5f8e883f
 
就这样,我们便成功的建立了一个指向最新一个提交的引用,引用名为master
在此之前我们查看提交记录需要执行 git log 491404,现在只需执行git log master。
$ git log 491404
commit 491404fa6e6f95eb14683c3c06d10ddc5f8e883f (HEAD -> master)
Author: john <john@163.com>
Date: Wed Sep 26 20:01:14 2018 +0800 

third commit 

commit e838c8678ef789df84c2666495663060c90975d7
Author: john <john@163.com>
Date: Wed Sep 26 19:47:22 2018 +0800 

second commit 

commit 7020a97c0e792f340e00e1bb8edcbafcc4dfb60f
Author: john <john@163.com>
Date: Wed Sep 26 19:31:18 2018 +0800 

first commit
$ git log master
commit 491404fa6e6f95eb14683c3c06d10ddc5f8e883f (HEAD -> master)
Author: john <john@163.com>
Date: Wed Sep 26 20:01:14 2018 +0800 

third commit 

commit e838c8678ef789df84c2666495663060c90975d7
Author: john <john@163.com>
Date: Wed Sep 26 19:47:22 2018 +0800 

second commit 

commit 7020a97c0e792f340e00e1bb8edcbafcc4dfb60f
Author: john <john@163.com>
Date: Wed Sep 26 19:31:18 2018 +0800 

first commit
 
结果完全相同。
Git并不提倡直接编辑引用文件,它提供了一个底层命令update-ref来创建或修改引用文件。
echo “491404fa6e6f95eb14683c3c06d10ddc5f8e883f” > .git/refs/heads/master 命令可以简单的写作:
$ git update-ref refs/heads/master 49140
这基本就是 Git 分支的本质:一个指向某一系列提交之首的指针或引用。
4. Git基本原理总结
 
Git的核心是它的对象数据库,其中保存着git的对象,其中最重要的是blob、tree和commit对象,blob对象实现了对文件内容的记录,tree对象实现了对文件名、文件目录结构的记录,commit对象实现了对版本提交时间、版本作者、版本序列、版本说明等附加信息的记录。这三类对象,完美实现了git的基础功能:对版本状态的记录。
Git引用是指向git对象hash键值的类似指针的文件。通过Git引用,我们可以更加方便的定位到某一版本的提交。Git分支、tags等功能都是基于Git引用实现的。

Rxjava基础

  现在很多Android App的开发开始使用Rxjava,但是Rxjava以学习曲线陡峭著称,入门有些困难。经过一段时间的学习和使用,这里来介绍一下我对Rxjava的理解。

  说到Rxjava首先需要了解的两个东西,一个是Observable(被观察者,事件源)和 Subscriber(观察者,是 Observer的子类)。Observable发出一系列事件,Subscriber处理这些事件。首先来看一个基本的例子,我们如何创建并使用Observable。

Observable.create(new Observable.OnSubscribe<String>() { 
	@Override public void call(Subscriber<? super String> subscriber) { 
		subscriber.onNext("hello"); 
		} 
	}).subscribe(new Subscriber<String>()
	{ 
		@Override public void onCompleted() {
		} 
		@Override public void onError(Throwable e) { 
		} 
		@Override public void onNext(String s) { 
			Log.d("rx-info", s); 
		} 
	});

  创建Observable的最基本的方法是通过Observable.create() 来进行,当有Subscriber通过Observable.subscribe() 方法进行订阅之后Observable就会发射事件,注意必须要有订阅者订阅才会发射事件。发射的方式是通过调用 Observable中的 OnSubsribe 类型的成员来实现(每个Observable有一个final OnSubscribe<T> onSubscribe 成员,该成员是一个接口,后面详细说),在 Onsubsribe类型成员中调用 call() 方法,注意,这个call方法的参数就是 Observable.subscribe() 方法传入的 Subsriber实例。总的一句话就是在Obsubscribe 的 call方法中执行订阅者(Subscriber)的三个方法 onNext(), onCompleted() 和 onError()。

  一开始就是一堆 Observable , Subscriber,subscribe() , OnSubscribe 估计看得头晕,因此我们需要先来对这些东西有一个了解。这里只列出一个帮助理解的大概。

public class Observable<T> {

	  final OnSubscribe<T> onSubscribe;
	  protected Observable(OnSubscribe<T> f) {
              this.onSubscribe = f;
          }

       public final static <T> Observable<T> create(OnSubscribe<T> f) {
            return new Observable<T>(hook.onCreate(f));
        }

       public interface OnSubscribe<T> extends Action1<Subscriber<? super T>> {
            // cover for generics insanity
        }
	
	  public final Subscription subscribe(Subscriber<? super T> subscriber) {
              return Observable.subscribe(subscriber, this);
          }

       public interface Operator<R, T> extends Func1<Subscriber<? super R>, Subscriber<? super T>> {
        // cover for generics insanity
       }
}
public interface Action1<T> extends Action {
    void call(T t);
}
public interface Subscription {
    void unsubscribe();
    boolean isUnsubscribed();
}
public interface Observer<T> {
    void onCompleted();
    void onError(Throwable e);
    void onNext(T t);
}

public abstract class Subscriber<T> implements Observer<T>, Subscription {
	//...
}

  通过上面的代码帮助理清楚 Observable, Observer, Subscriber, OnSubsriber, subscribe() 之间的关系。这里额外提一下 Observable.subscribe() 方法有多个重载:

Subscription    subscribe()
Subscription    subscribe(Action1<? super  T> onNext)
Subscription    subscribe(Action1<? super  T> onNext, Action1< java.lang .Throwable> onError)
Subscription    subscribe(Action1<? super  T> onNext, Action1< java.lang .Throwable> onError, Action0 onComplete)
Subscription    subscribe(Observer<? super  T> observer)
Subscription    subscribe(Subscriber<? super  T> subscriber)

  其它的ActionX 和 FuncX 请大家自行去查阅定义。

  介绍了基本的创建Observable和 Observable是怎么发射事件的之后,来介绍一下Rxjava的Operator和Operator的原理。

  Rxjava的Operator常见的有map, flatMap, concat, merge之类的。这里就不介绍Operator的使用了,介绍一下其原理。介绍原理还是来看源码,以map为例。

  首先看一下使用map的例子:

Observable.create(new Observable.OnSubscribe<String>() {
    @Override
    public void call(Subscriber<? super String> subscriber) {
        subscriber.onNext("hello");
    }
})
.map(new Func1<String, String>() {
    @Override
    public String call(String s) {
        return s + "word";
    }
})
.subscribe(new Subscriber<String>() {
    @Override
    public void onCompleted() {

    }

    @Override
    public void onError(Throwable e) {

    }

    @Override
    public void onNext(String s) {
        Log.d("info-rx", s);
    }
});

  继续来看 map的定义:

    public final <R> Observable<R> map(Func1<? super T, ? extends R> func) {
        return lift(new OperatorMap<T, R>(func));
    }

  简单说一下Func1,其中的T表示传入的参数类型,R表示方法返回的参数类型。Operator的操作原理最核心的就是lift的实现。

    public final <R> Observable<R> lift(final Operator<? extends R, ? super T> operator) {
        return new Observable<R>(new OnSubscribe<R>() {
            @Override
            public void call(Subscriber<? super R> o) {
                try {
                    Subscriber<? super T> st = hook.onLift(operator).call(o);
                    try {
                        // new Subscriber created and being subscribed with so 'onStart' it
                        st.onStart();
                        onSubscribe.call(st);
                    } catch (Throwable e) {
                        // localized capture of errors rather than it skipping all operators 
                        // and ending up in the try/catch of the subscribe method which then
                        // prevents onErrorResumeNext and other similar approaches to error handling
                        Exceptions.throwIfFatal(e);
                        st.onError(e);
                    }
                } catch (Throwable e) {
                    Exceptions.throwIfFatal(e);
                    // if the lift function failed all we can do is pass the error to the final Subscriber
                    // as we don't have the operator available to us
                    o.onError(e);
                }
            }
        });
    }

  lift方法看起来太过复杂,稍作简化:

public final <R> Observable<R> lift(final Operator<? extends R, ? super T> operator) {
    return new Observable<R>(...);
}

  lift方法实际是产生一个新的 Observable。在map()调用之后,我们操作的就是新的Observable对象,我们可以把它取名为Observable$2,我们这里调用subscribe就是Observable$2.subscribe,继续看到subscribe里,重要的几个调用:

hook.onSubscribeStart(observable, observable.onSubscribe).call(subscriber);
return hook.onSubscribeReturn(subscriber);

  注意,这里的observable是Observable$2!!也就是说,这里的onSubscribe是,lift中定义的!!

  回过头来看lift方法中创建新Observable的过程:

return new Observable<R>(new OnSubscribe<R>() {
    @Override
    public void call(Subscriber<? super R> o) {
        try {
            Subscriber<? super T> st = hook.onLift(operator).call(o);
            try {
                // new Subscriber created and being subscribed with so 'onStart' it
                st.onStart();
                onSubscribe.call(st); //请注意我!! 这个onSubscribe是原始的OnSubScribe对象!!
            } catch (Throwable e) {
                // localized capture of errors rather than it skipping all operators 
                // and ending up in the try/catch of the subscribe method which then
                // prevents onErrorResumeNext and other similar approaches to error handling
                if (e instanceof OnErrorNotImplementedException) {
                    throw (OnErrorNotImplementedException) e;
                }
                st.onError(e);
            }
        } catch (Throwable e) {
            if (e instanceof OnErrorNotImplementedException) {
                throw (OnErrorNotImplementedException) e;
            }
            // if the lift function failed all we can do is pass the error to the final Subscriber
            // as we don't have the operator available to us
            o.onError(e);
        }
    }
});

  一定一定要注意这段函数执行的上下文!,这段函数中的onSubscribe对象指向的是外部类,也就是第一个Observable的onSubScribe!而不是Observable$2中的onSubscribe,接下来看:

Subscriber<? super T> st = hook.onLift(operator).call(o);

  这行代码,就是定义operator,生成一个经过operator操作过的Subscriber,看下OperatorMap这个类中的call方法: 

@Override
public Subscriber<? super T> call(final Subscriber<? super R> o) {
    return new Subscriber<T>(o) {

        @Override
        public void onCompleted() {
            o.onCompleted();
        }

        @Override
        public void onError(Throwable e) {
            o.onError(e);
        }

        @Override
        public void onNext(T t) {
            try {
                o.onNext(transformer.call(t));
            } catch (Throwable e) {
                Exceptions.throwIfFatal(e);
                onError(OnErrorThrowable.addValueAsLastCause(e, t));
            }
        }

    };
}

  没错,对传入的Subscriber做了一个代理,把转换后的值传入。这样就生成了一个代理的Subscriber,最后我们最外层的OnSubscribe对象对我们代理的Subscriber进行了调用: 

 @Override
public void call(Subscriber<? super String> subscriber) {
    //此处的subscriber就是被map包裹(wrapper)后的对象。
    subscriber.onNext("hello");
}

  然后这个subscriber传入到内部,链式的通知,最后通知到我们在subscribe函数中定义的对象。

  分析lift的原理,其实还是回到了一开始介绍的Observable必须要有订阅者进行订阅才能发射事件。lift方法会产生一个新的Observable,并且这个Observable位于原始Observable和后面的Subsriber之间,因此lift方法也需要提供一个新的Subscriber来使得新产生的Observable发射事件,这个新的Subsriber就是对事件链后方的Subscriber就行包装做一个代理。

  详细使用Rxjava可参见:

  1. 给 Android 开发者的 RxJava 详解

  2.Rxjava使用基础合集

 

Android 网络通信API的选择和实现实例

  Android开发网络通信一开始的时候使用的是AsyncTask封装HttpClient,没有使用原生的HttpURLConnection就跳到了Volley,随着OkHttp的流行又开始迁移到OkHttp上面,随着Rxjava的流行又了解了Retrofit,随着Retrofit的发展又从1.x到了2.x……。好吧,暂时到这里。

  那么的多的使用工具有时候有点眼花缭乱,今天来总结一下现在比较流行的基于OkHttp 和 Retrofit 的网络通信API设计方法。有些同学可能要想,既然都有那么好用的Volley和Okhttp了,在需要用到的地方创建一个Request然后交给RequestQueue(Volley的方式)或者 Call(Okhttp的方式)就行了吗,为什么还那么麻烦? 但是我认为这种野生的网络库的用法还是是有很多弊端(弊端就不说了,毕竟是总结新东西),在好的Android架构中都不会出现这样的代码。

  网络通信都是异步完成,设计网络API我觉得首先需要考虑异步结果的返回机制。基于Okhttp或Retrofit,我们考虑如何返回异步的返回结果,有几种方式:

  1. 直接返回:

  OkHttp 的返回方式:

OkHttpClient : OkHttpClient client = new OkHttpClient();

Request :  Request request = new Request.Builder()
                                        .url("https://api.github.com/repos/square/okhttp/issues")
                                        .header("User-Agent", "OkHttp Headers.java")
                                        .addHeader("Accept", "application/json; q=0.5")
                                        .addHeader("Accept", "application/vnd.github.v3+json")
                                        .build();
                                                 
//第一种
Response response = client.newCall(request).execute();
// 第二种
client.newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Request request, Throwable throwable) {
                                      
    }
    @Override public void onResponse(Response response) throws IOException {

    }                                     
}

  Retrofit 的方式:

interface GitHubService {
  @GET("/repos/{owner}/{repo}/contributors")
  Call<List<Contributor>> repoContributors(
      @Path("owner") String owner,
      @Path("repo") String repo);
}
Call<List<Contributor>> call =
    gitHubService.repoContributors("square", "retrofit");

response = call.execute();

  上面的方式适用于野生的返回网络请求的内容。

  2. 使用事件总线(Otto,EventBus,RxBus(自己使用PublishSubject封装))

  代码来源:https://github.com/saulmm/Material-Movies

public interface MovieDatabaseAPI { /************Retrofit 1.x ,使用异步的方式返回 ****************/

    @GET("/movie/popular")
    void getPopularMovies(
        @Query("api_key") String apiKey,
        Callback<MoviesWrapper> callback);

    @GET("/movie/{id}")
    void getMovieDetail (
        @Query("api_key") String apiKey,
        @Path("id") String id,
        Callback<MovieDetail> callback
    );

    @GET("/movie/popular")
    void getPopularMoviesByPage(
        @Query("api_key") String apiKey,
        @Query("page") String page,
        Callback<MoviesWrapper> callback
    );

    @GET("/configuration")
    void getConfiguration (
        @Query("api_key") String apiKey,
        Callback<ConfigurationResponse> response
    );

    @GET("/movie/{id}/reviews")
    void getReviews (
        @Query("api_key") String apiKey,
        @Path("id") String id,
        Callback<ReviewsWrapper> response
    );

    @GET("/movie/{id}/images")
    void getImages (
        @Query("api_key") String apiKey,
        @Path("id") String movieId,
        Callback<ImagesWrapper> response
    );
}

  

public class RestMovieSource implements RestDataSource {

    private final MovieDatabaseAPI moviesDBApi;
    private final Bus bus; /***********使用了Otto**************/

    public RestMovieSource(Bus bus) {

        RestAdapter movieAPIRest = new RestAdapter.Builder() /*** Retrofit 1.x ***/
            .setEndpoint(Constants.MOVIE_DB_HOST)
            .setLogLevel(RestAdapter.LogLevel.HEADERS_AND_ARGS)
            .build();

        moviesDBApi = movieAPIRest.create(MovieDatabaseAPI.class);
        this.bus = bus;
    }

    @Override
    public void getMovies() {

        moviesDBApi.getPopularMovies(Constants.API_KEY, retrofitCallback);
    }

    @Override
    public void getDetailMovie(String id) {

        moviesDBApi.getMovieDetail(Constants.API_KEY, id,
            retrofitCallback);
    }

    @Override
    public void getReviews(String id) {

        moviesDBApi.getReviews(Constants.API_KEY, id,
            retrofitCallback);
    }

    @Override
    public void getConfiguration() {

        moviesDBApi.getConfiguration(Constants.API_KEY, retrofitCallback);
    }

    @Override
    public void getImages(String movieId) {

        moviesDBApi.getImages(Constants.API_KEY, movieId,
            retrofitCallback);
    }

    public Callback retrofitCallback = new Callback() { /******************这里统一的Callback,根据不同的返回值使用事件总线进行返回**************************/
        @Override
        public void success(Object o, Response response) {

            if (o instanceof MovieDetail) {

                MovieDetail detailResponse = (MovieDetail) o;
                bus.post(detailResponse);

            } else if (o instanceof MoviesWrapper) {

                MoviesWrapper moviesApiResponse = (MoviesWrapper) o;
                bus.post(moviesApiResponse);

            } else if (o instanceof ConfigurationResponse) {

                ConfigurationResponse configurationResponse = (ConfigurationResponse) o;
                bus.post(configurationResponse);

            } else if (o instanceof ReviewsWrapper) {

                ReviewsWrapper reviewsWrapper = (ReviewsWrapper) o;
                bus.post(reviewsWrapper);

            } else if (o instanceof ImagesWrapper) {

                ImagesWrapper imagesWrapper = (ImagesWrapper) o;
                bus.post(imagesWrapper);
            }
        }

        @Override
        public void failure(RetrofitError error) {

            System.out.printf("[DEBUG] RestMovieSource failure - " + error.getMessage());
        }
    };

    @Override
    public void getMoviesByPage(int page) {

        moviesDBApi.getPopularMoviesByPage(
            Constants.API_KEY,
            page + "",
            retrofitCallback
        );
    }
}

  

  3. 返回Observable(这里也可以考虑直接返回Observable 和间接返回Observable)

  直接的返回 Observable,在创建 apiService 的时候使用 Retrofit.create(MovieDatabaseAPI)就行了(见下面代码)

public interface MovieDatabaseAPI {

    @GET("/movie/popular")
    Observable<MovieWrapper> getPopularMovies(
        @Query("api_key") String apiKey,
        );

    @GET("/movie/{id}")
    Observable<MovideDetail> getMovieDetail (
        @Query("api_key") String apiKey,
        @Path("id") String id,
    );
}  

  间接返回Observable,这里参考了AndroidCleanArchitecture:

public interface RestApi {   /************定义API接口*****************/
  String API_BASE_URL = "http://www.android10.org/myapi/";

  /** Api url for getting all users */
  String API_URL_GET_USER_LIST = API_BASE_URL + "users.json";
  /** Api url for getting a user profile: Remember to concatenate id + 'json' */
  String API_URL_GET_USER_DETAILS = API_BASE_URL + "user_";

  /**
   * Retrieves an {@link rx.Observable} which will emit a List of {@link UserEntity}.
   */
  Observable<List<UserEntity>> userEntityList();

  /**
   * Retrieves an {@link rx.Observable} which will emit a {@link UserEntity}.
   *
   * @param userId The user id used to get user data.
   */
  Observable<UserEntity> userEntityById(final int userId);
}

  

/**** 使用Rx Observable 实现 RestApi 接口,实际调用的是 ApiConnection 里面的方法  ****/
public class RestApiImpl implements RestApi { /***注意这里没有使用Retrofit,而是对上面接口的实现***/

    private final Context context;
    private final UserEntityJsonMapper userEntityJsonMapper;

    /**
     * Constructor of the class
     *
     * @param context {@link android.content.Context}.
     * @param userEntityJsonMapper {@link UserEntityJsonMapper}.
     */
    public RestApiImpl(Context context, UserEntityJsonMapper userEntityJsonMapper) {
        if (context == null || userEntityJsonMapper == null) {
            throw new IllegalArgumentException("The constructor parameters cannot be null!!!");
        }
        this.context = context.getApplicationContext();
        this.userEntityJsonMapper = userEntityJsonMapper;
    }

    @RxLogObservable(SCHEDULERS)
    @Override
    public Observable<List<UserEntity>> userEntityList() {
        return Observable.create(subscriber -> {
            if (isThereInternetConnection()) {
                try {
                    String responseUserEntities = getUserEntitiesFromApi();
                    if (responseUserEntities != null) {
                        subscriber.onNext(userEntityJsonMapper.transformUserEntityCollection(
                                responseUserEntities));
                        subscriber.onCompleted();
                    } else {
                        subscriber.onError(new NetworkConnectionException());
                    }
                } catch (Exception e) {
                    subscriber.onError(new NetworkConnectionException(e.getCause()));
                }
            } else {
                subscriber.onError(new NetworkConnectionException());
            }
        });
    }

    @RxLogObservable(SCHEDULERS)
    @Override
    public Observable<UserEntity> userEntityById(final int userId) {
        return Observable.create(subscriber -> {
            if (isThereInternetConnection()) {
                try {
                    String responseUserDetails = getUserDetailsFromApi(userId);
                    if (responseUserDetails != null) {
                        subscriber.onNext(userEntityJsonMapper.transformUserEntity(responseUserDetails));
                        subscriber.onCompleted();
                    } else {
                        subscriber.onError(new NetworkConnectionException());
                    }
                } catch (Exception e) {
                    subscriber.onError(new NetworkConnectionException(e.getCause()));
                }
            } else {
                subscriber.onError(new NetworkConnectionException());
            }
        });
    }

    private String getUserEntitiesFromApi() throws MalformedURLException {
        return ApiConnection.createGET(RestApi.API_URL_GET_USER_LIST).requestSyncCall();
    }

    private String getUserDetailsFromApi(int userId) throws MalformedURLException {
        String apiUrl = RestApi.API_URL_GET_USER_DETAILS + userId + ".json";
        return ApiConnection.createGET(apiUrl).requestSyncCall();
    }

    /**
     * Checks if the device has any active internet connection.
     *
     * @return true device with internet connection, otherwise false.
     */
    private boolean isThereInternetConnection() {
        boolean isConnected;

        ConnectivityManager connectivityManager =
                (ConnectivityManager) this.context.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
        isConnected = (networkInfo != null && networkInfo.isConnectedOrConnecting());

        return isConnected;
    }
}

  

public class ApiConnection implements Callable<String> {  /***********************网络接口的实际实现********************************/

    private static final String CONTENT_TYPE_LABEL = "Content-Type";
    private static final String CONTENT_TYPE_VALUE_JSON = "application/json; charset=utf-8";

    private URL url;
    private String response;

    private ApiConnection(String url) throws MalformedURLException {
        this.url = new URL(url);
    }

    public static ApiConnection createGET(String url) throws MalformedURLException {
        return new ApiConnection(url);
    }

    /**
     * Do a request to an api synchronously.
     * It should not be executed in the main thread of the application.
     *
     * @return A string response
     */
    @Nullable
    public String requestSyncCall() {
        connectToApi();
        return response;
    }

    private void connectToApi() {
        OkHttpClient okHttpClient = this.createClient(); /*******************使用OKhttp的实现*******************/
        final Request request = new Request.Builder()
                .url(this.url)
                .addHeader(CONTENT_TYPE_LABEL, CONTENT_TYPE_VALUE_JSON)
                .get()
                .build();

        try {
            this.response = okHttpClient.newCall(request).execute().body().string();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private OkHttpClient createClient() {
        final OkHttpClient okHttpClient = new OkHttpClient();
        okHttpClient.setReadTimeout(10000, TimeUnit.MILLISECONDS);
        okHttpClient.setConnectTimeout(15000, TimeUnit.MILLISECONDS);

        return okHttpClient;
    }

    @Override
    public String call() throws Exception {
        return requestSyncCall();
    }
}

  这里简单总结了一下OkHttp和Retrofit该如何封装,这样的封装放在整个大的代码框架中具有很好的模块化效果。对于使用MVP架构或者类似架构的APP,良好的网络接口模块封装是非常重要的。

 

OkHttp,Retrofit 1.x – 2.x 基本使用

  Square 为广大开发者奉献了OkHttp,Retrofit1.x,Retrofit2.x,运用比较广泛,这三个工具有很多相似之处,初学者可能会有一些使用迷惑。这里来总结一下它们的一些基本使用和一些细微差别。

/**************
Retrofit 基本使用方法

Retrofit 到底是返回什么? void, Observable, Call?

*************/
/********************************************Retrofit****************************************************************/
/*** 同步调用的方式  ****/
interface GitHubService {
  @GET("/repos/{owner}/{repo}/contributors")
  List<Contributor> repoContributors(
      @Path("owner") String owner,
      @Path("repo") String repo);
} 

List<Contributor> contributors =
    gitHubService.repoContributors("square", "retrofit");
/***** 异步调用的方式 仅限于 Retrofit 1.x !!!!!!! *****/
interface GitHubService {
  @GET("/repos/{owner}/{repo}/contributors")
  void repoContributors(
      @Path("owner") String owner,
      @Path("repo") String repo,
      Callback<List<Contributor>> cb); // 异步调用添加 CallBack
} 

service.repoContributors("square", "retrofit", new Callback<List<Contributor>>() {
  @Override void success(List<Contributor> contributors, Response response) {
    // ...
  }


  @Override void failure(RetrofitError error) {
    // ...
  }
});

/**** Rxjava 方式 ****/
interface GitHubService {
  @GET("/repos/{owner}/{repo}/contributors")
  Observable<List<Contributor>> repoContributors(
      @Path("owner") String owner,
      @Path("repo") String repo);
} 
// 调用
gitHubService.repoContributors("square", "retrofit")
    .subscribe(new Action1<List<Contributor>>() {
      @Override public void call(List<Contributor> contributors) {
        // ...
      }
    });
    
/*******************************注意以下三个Callback的不同***************************************/
	
// Retrofit Callback Version 1.9 
public interface Callback<T> {

  /** Successful HTTP response. */
  void success(T t, Response response);

  /**
   * Unsuccessful HTTP response due to network failure, non-2XX status code, or unexpected
   * exception.
   */
  void failure(RetrofitError error);
}
// Retrofit Callback Version 2.0	!!!!!!!!!
public interface Callback<T> {
  /** Successful HTTP response. */
  void onResponse(Response<T> response, Retrofit retrofit);

  /** Invoked when a network or unexpected exception occurred during the HTTP request. */
  void onFailure(Throwable t);
}
// OkHttp	
public interface Callback {
  void onFailure(Request request, IOException e);

  void onResponse(Response response) throws IOException; // 注意参数不同
}



/*********************************回顾一下Okhttp的调用方式********************************************/

//1. 创建 
OkHttpClient : OkHttpClient client = new OkHttpClient();
//2. 创建 
Request :  Request request = new Request.Builder()
									    .url("https://api.github.com/repos/square/okhttp/issues")
										.header("User-Agent", "OkHttp Headers.java")
										.addHeader("Accept", "application/json; q=0.5")
										.addHeader("Accept", "application/vnd.github.v3+json")
										.build();
												
//3. 使用 client 执行请求(两种方式):  
//第一种,同步执行
Response response = client.newCall(request).execute();
// 第二种,异步执行方式
client.newCall(request).enqueue(new Callback() { 
    @Override 
    public void onFailure(Request request, Throwable throwable) {
    // 复写该方法
											 
    }
    @Override public void onResponse(Response response) throws IOException {
	// 复写该方法
    }									   
}
	
	
/***********************************Retrofit 1.0 不能获得 Header 或者整个 Body*****************************************/
/**********引入 Call , 每个Call只能调用一次,可以使用Clone方法来生成一次调用多次,使用Call既可以同步也可以异步*********/

interface GitHubService {
  @GET("/repos/{owner}/{repo}/contributors")
  Call<List<Contributor>> repoContributors(
      @Path("owner") String owner,
      @Path("repo") String repo);
}

Call<List<Contributor>> call =
    gitHubService.repoContributors("square", "retrofit");

response = call.execute(); /*************** 同步的方式调用,注意这里返回了 Response 后面会提到 ********************/

// This will throw IllegalStateException: 每个Call只能执行一次
response = call.execute();

Call<List<Contributor>> call2 = call.clone(); // 调用Clone之后又可以执行
// This will not throw:
response = call2.execute();

/************************ 异步的方式调用 *********************************/

Call<List<Contributor>> call =
    gitHubService.repoContributors("square", "retrofit");

call.enqueue(new Callback<List<Contributor>>() {
  @Override void onResponse(/* ... */) {
    // ...
  }

  @Override void onFailure(Throwable t) {
    // ...
  }
});

/****************************引入 Response,获取返回的RawData,包括:response code, response message, headers**********************************/

class Response<T> {
  int code();
  String message();
  Headers headers();

  boolean isSuccess(); 
  T body();
  ResponseBody errorBody(); 
  com.squareup.okhttp.Response raw();
}

interface GitHubService {
  @GET("/repos/{owner}/{repo}/contributors")
  Call<List<Contributor>> repoContributors(
      @Path("owner") String owner,
      @Path("repo") String repo);
} 

Call<List<Contributor>> call = 
    gitHubService.repoContributors("square", "retrofit");
Response<List<Contributor>> response = call.execute(); 

/*********************************** Dynamic URL *****************************************/

interface GitHubService {
  @GET("/repos/{owner}/{repo}/contributors")
  Call<List<Contributor>> repoContributors(
      @Path("owner") String owner,
      @Path("repo") String repo);

  @GET
  Call<List<Contributor>> repoContributorsPaginate(
      @Url String url);// 直接填入 URL 而不是在GET中替换字段的方式
}

/*************************************根据返回值实现重载*****************************************************/
interface SomeService {
  @GET("/some/proto/endpoint")
  Call<SomeProtoResponse> someProtoEndpoint(); // SomeProtoResponse

  @GET("/some/json/endpoint")
  Call<SomeJsonResponse> someJsonEndpoint(); // SomeJsonResponse
}

interface GitHubService {
  @GET("/repos/{owner}/{repo}/contributors")
  Call<List<Contributor>> repoContributors(..);

  @GET("/repos/{owner}/{repo}/contributors")
  Observable<List<Contributor>> repoContributors2(..);

  @GET("/repos/{owner}/{repo}/contributors")
  Future<List<Contributor>> repoContributors3(..); // 可以返回 Future
}

/******************************************Retrofit 1.x Interceptor,添加头部信息的时候经常用到Interceptor*************************************************************/
    RestAdapter.Builder builder = new RestAdapter.Builder().setRequestInterceptor(new RequestInterceptor() {
        @Override
        public void intercept(RequestFacade request) {
            request.addHeader("Accept", "application/json;versions=1");
        }
    });


/******************************************Retrofit 2.x Interceptor**************************************************/            
    
OkHttpClient client = new OkHttpClient();
client.interceptors().add(new Interceptor() {    
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request original = chain.request();
        
        Request request = original.newBuilder()
                                  .header("Accept", "application/json")
                                  .header("Authorization", "auth-token")
                                  .method(original.method(), original.body())
                                  .build();
       
       Response response = chain.proceed(request);
       return response;      
        
    }   
}

Retrofit retrofit = Retrofit.Builder()
            .baseUrl("https://your.api.url/v2/")
            .client(client).build();


/***************************************异步实例*********************************************/
public interface APIService {

    @GET("/users/{user}/repos")
    Call<List<Repo>> listRepos(@Path("user") String user);

    @GET("/users/{user}/repos")
    Call<String> listReposStr(@Path("user") String user);
//错误,不能这样使用异步
//    @GET("/users/{user}/repos")
//    void listRepos(@Path("user") String user, Callback<List<Repo>> callback);
}

private void prepareServiceAPI() {
    //For logging
    HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
    logging.setLevel(HttpLoggingInterceptor.Level.BODY);

    OkHttpClient client = new OkHttpClient();
    client.interceptors().add(new MyInterceptor());
    client.interceptors().add(logging);
	// setUp Retrofit 
    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl("https://api.github.com")
            //.addConverterFactory(new ToStringConverterFactory())
            .addConverterFactory(GsonConverterFactory.create())
            .client(client)
            .build();

    service = retrofit.create(APIService.class);
}
// 异步调用
public void execute() {
    Call<List<Repo>> call = service.listRepos("pasha656");
    call.enqueue(new Callback<List<Repo>>() {
        @Override
        public void onResponse(Response<List<Repo>> response, Retrofit retrofit) {

            if (response.isSuccess()) {
                if (!response.body().isEmpty()) {
                    StringBuilder sb = new StringBuilder();
                    for (Repo r : response.body()) {
                        sb.append(r.getId()).append(" ").append(r.getName()).append(" /n");
                    }
                    activity.setText(sb.toString());
                }
            } else {
                APIError error = ErrorUtils.parseError(response, retrofit);
                Log.d("Pasha", "No succsess message "+error.getMessage());
            }


            if (response.errorBody() != null) {
                try {
                    Log.d("Pasha", "Error "+response.errorBody().string());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        @Override
        public void onFailure(Throwable t) {
            Log.d("Pasha", "onFailure "+t.getMessage());
        }
    });
}

  

Dagger2 生成代码学习

  接上一篇文章介绍了Dagger2的初步使用,相信刚接触的人会觉得很奇怪,怎么会有很多自己没有定义的代码出现,为什么Component的创建方式是那样的。为了搞清楚这些东西,我们需要查看一下Dagger2 生成的源代码。Dagger2 是一个DI框架,通过学习生成的代码也可以更好的理解Dagger2是如何做依赖注入的。

  将上一篇文章中的工程在SublimeText中打开,结构如下图:

Dagger2 生成代码学习

  可以看到AppComponent 生成了 DaggerAppComponent,Dagger2的生成规则中,我们自定义的Component生成之后会加上前缀“Dagger”。此外Module中的每个@Provides 方法都会生成一个Factory 类,命名规则也是很规律的。这里Dagger一共为我们生成了6个类:DaggerAppComponent / AppModule_ProvideApplicationFactory /ReposListActivity_MembersInjector / GithubApiModule_ProvideRetrofitFactory / GithubApiModule_ProvideOkHttpClientFactory / GithubApiModule_ProvideGitHubServiceFactory。接下来我们看看这些类具体是什么样的。

  首先来看一下DaggerAppComponent:

@Generated("dagger.internal.codegen.ComponentProcessor")
public final class DaggerAppComponent implements AppComponent {
  private Provider<Application> provideApplicationProvider;
  private Provider<OkHttpClient> provideOkHttpClientProvider;
  private Provider<Retrofit> provideRetrofitProvider;
  private Provider<GithubApiService> provideGitHubServiceProvider;
  private MembersInjector<ReposListActivity> reposListActivityMembersInjector;

  private DaggerAppComponent(Builder builder) {  
    assert builder != null;
    initialize(builder);
  }

  public static Builder builder() {  
    return new Builder();
  }

  private void initialize(final Builder builder) {  
    this.provideApplicationProvider = AppModule_ProvideApplicationFactory.create(builder.appModule);
    this.provideOkHttpClientProvider = GithubApiModule_ProvideOkHttpClientFactory.create(builder.githubApiModule);
    this.provideRetrofitProvider = GithubApiModule_ProvideRetrofitFactory.create(builder.githubApiModule, provideApplicationProvider, provideOkHttpClientProvider);
    this.provideGitHubServiceProvider = GithubApiModule_ProvideGitHubServiceFactory.create(builder.githubApiModule, provideRetrofitProvider);
    this.reposListActivityMembersInjector = ReposListActivity_MembersInjector.create((MembersInjector) MembersInjectors.noOp(), provideGitHubServiceProvider);
  }

  @Override
  public void inject(ReposListActivity activity) {  
    reposListActivityMembersInjector.injectMembers(activity);
  }

  public static final class Builder {
    private AppModule appModule;
    private GithubApiModule githubApiModule;
  
    private Builder() {  
    }
  
    public AppComponent build() {  
      if (appModule == null) {
        throw new IllegalStateException("appModule must be set");
      }
      if (githubApiModule == null) {
        this.githubApiModule = new GithubApiModule();
      }
      return new DaggerAppComponent(this);
    }
  
    public Builder appModule(AppModule appModule) {  
      if (appModule == null) {
        throw new NullPointerException("appModule");
      }
      this.appModule = appModule;
      return this;
    }
  
    public Builder githubApiModule(GithubApiModule githubApiModule) {  
      if (githubApiModule == null) {
        throw new NullPointerException("githubApiModule");
      }
      this.githubApiModule = githubApiModule;
      return this;
    }
  }
}

  我们在Application中实例化AppComponent的时候是这样的:

appComponent = DaggerAppComponent.builder()
        .githubApiModule(new GithubApiModule())
        .appModule(new AppModule(this))
        .build();

  第一次见的时候肯定觉得一团雾水,为什么要这样写呢? 通过上面DaggerAppcomponent代码可以看出,使用了建造者模式。我们回顾一下AppComponent是怎么样的:

@Component(modules = { AppModule.class, GithubApiModule.class})
public interface AppComponent {
    // inject what
    void inject(ReposListActivity activity);
}

  @component 后面modules包含了两个类,在生成的DaggerAppComponent 中的内部类 Builder中,AppModule.class 和 GithubApiModule.class 成为了其成员,并且提供了set方法。在Application中实例化DaggerAppComponent的过程中其实就是调用了其set方法来设置依赖。此外在DaggerAppComponent各个@Provides 注解的方法返回类型都是其一个成员变量,并在Initialize()方法中创建。此外还有一个MembersInjector成员,DaggerAppComponent也要负责创建它。

@Generated("dagger.internal.codegen.ComponentProcessor")
public final class ReposListActivity_MembersInjector implements MembersInjector<ReposListActivity> {
  private final MembersInjector<BaseActivity> supertypeInjector;
  private final Provider<GithubApiService> githubApiServiceProvider;

  public ReposListActivity_MembersInjector(MembersInjector<BaseActivity> supertypeInjector, Provider<GithubApiService> githubApiServiceProvider) {  
    assert supertypeInjector != null;
    this.supertypeInjector = supertypeInjector;
    assert githubApiServiceProvider != null;
    this.githubApiServiceProvider = githubApiServiceProvider;
  }

  @Override
  public void injectMembers(ReposListActivity instance) {  
    if (instance == null) {
      throw new NullPointerException("Cannot inject members into a null reference");
    }
    supertypeInjector.injectMembers(instance);
    instance.githubApiService = githubApiServiceProvider.get();
  }

  public static MembersInjector<ReposListActivity> create(MembersInjector<BaseActivity> supertypeInjector, Provider<GithubApiService> githubApiServiceProvider) {  
      return new ReposListActivity_MembersInjector(supertypeInjector, githubApiServiceProvider);
  }
}

  ReposListActivity_MembersInjector 中通过 injectMembers 方法获取到 ReposListActivity 的实例(对应的就是 ReposListActivityComponent中的inject),然后进行赋值。从这种赋值的方式来看被@Inject注解的注入对象不能是private的。在看赋值是通过 githubAPiServiceProvider.get() 方法获取的,githubApiServiceProvider是一个工厂类,我们来看看这个工厂类是怎么样的:

@Generated("dagger.internal.codegen.ComponentProcessor")
public final class GithubApiModule_ProvideGitHubServiceFactory implements Factory<GithubApiService> {
  private final GithubApiModule module;
  private final Provider<Retrofit> retrofitProvider;

  public GithubApiModule_ProvideGitHubServiceFactory(GithubApiModule module, Provider<Retrofit> retrofitProvider) {  
    assert module != null;
    this.module = module;
    assert retrofitProvider != null;
    this.retrofitProvider = retrofitProvider;
  }。

  @Override
  public GithubApiService get() {  
    GithubApiService provided = module.provideGitHubService(retrofitProvider.get());
    if (provided == null) {
      throw new NullPointerException("Cannot return null from a non-@Nullable @Provides method");
    }
    return provided;
  }

  public static Factory<GithubApiService> create(GithubApiModule module, Provider<Retrofit> retrofitProvider) {  
    return new GithubApiModule_ProvideGitHubServiceFactory(module, retrofitProvider);
  }
}

  GithubApiModule_ProvideGitHubServiceFactory 类中有两个成员,一个是提供(@Provides)GithubService 所在的 module 类,一个是创建GithubService方法所需参数的retrofitProvider(从这里可以看出Dagger2需要创建retrofitProvider的工厂)。然后通过 module.provdeGitHubService()方法来创建 GithubApiService实例,这样最终穿件了在ReposListActivity中注入的依赖实例。

  Dagger2 入门有点绕,也没找到什么系统性的资料,但是可以通过查看框架自动生成的类来加深理解,方便大家使用。使用到dependencies和SubComponent注解的时候生成的代码就比较多了,本来也想捋捋的,但是原理和上面的都是一样的,大家自己去看吧。

 

Dagger2 使用初步

  Dagger2 是一个Android依赖注入框架,由谷歌开发,最早的版本Dagger1 由Square公司开发。依赖注入框架主要用于模块间解耦,提高代码的健壮性和可维护性。Dagger 这个库的取名不仅仅来自它的本意“匕首”,同时也暗示了它的原理。Jake Wharton 在对 Dagger 的介绍中指出,Dagger 即 DAG-er,这里的 DAG 即数据结构中的 DAG——有向无环图(Directed Acyclic Graph)。也就是说,Dagger 是一个基于有向无环图结构的依赖注入库,因此Dagger的使用过程中不能出现循环依赖。

  Android开发从一开始的MVC框架,到MVP,到MVVM,不断变化。现在MVVM的data-binding还在实验阶段,传统的MVC框架Activity内部可能包含大量的代码,难以维护,现在主流的架构还是使用MVP(Model + View + Presenter)的方式。但是 MVP 框架也有可能在Presenter中集中大量的代码,引入DI框架Dagger2 可以实现 Presenter 与 Activity 之间的解耦,Presenter和其它业务逻辑之间的解耦,提高模块化和可维护性。

  说了那么多,那什么是依赖呢?如果在 Class A 中,有 Class B 的实例,则称 Class A 对 Class B 有一个依赖。例如下面类 Human 中用到一个 Father 对象,我们就说类 Human 对类 Father 有一个依赖(参考)。

public class Human {
    ...
    Father father;
    ...
    public Human() {
        father = new Father();
    }
}

  那什么又是依赖注入呢,依赖注入就是非自己主动初始化依赖,而通过外部来传入依赖的方式,简单来说就是不使用 new 来创建依赖对象。使用 Dagger2 创建依赖对象,我们就不用手动初始化了。个人认为 Dagger2 和 MVP 架构是比较不错的搭配,Activity 依赖的 Presenter 可以使用该DI框架直接生成,实现解耦,简单的使用方式如下:

public class MainActivity extends BaseActivity {
      @Inject
       MainActivityPresenter presenter;

     ...  
    
}

  上面这些主要是对DI框架有一个初步全局的了解,下面来看看Dagger2的基本内容。Dagger2 通过注解来生成代码,定义不同的角色,主要的注解有:@Inject、@Module 、@Component 、@Provides 、@Scope 、@SubComponent 等。

  @Inject: 通常在需要依赖的地方使用这个注解。换句话说,你用它告诉Dagger这个类或者字段需要依赖注入。这样,Dagger就会构造一个这个类的实例并满足他们的依赖。
  @Module: Modules类里面的方法专门提供依赖,所以我们定义一个类,用@Module注解,这样Dagger在构造类的实例的时候,就知道从哪里去找到需要的 依赖。modules的一个重要特征是它们设计为分区并组合在一起(比如说,在我们的app中可以有多个组成在一起的modules)。
  @Provides: 在modules中,我们定义的方法是用这个注解,以此来告诉Dagger我们想要构造对象并提供这些依赖。
  @Component: Components从根本上来说就是一个注入器,也可以说是@Inject和@Module的桥梁,它的主要作用就是连接这两个部分。 Components可以提供所有定义了的类型的实例,比如:我们必须用@Component注解一个接口然后列出所有的   @Modules组成该组件,如 果缺失了任何一块都会在编译的时候报错。所有的组件都可以通过它的modules知道依赖的范围。
  @Scope: Scopes可是非常的有用,Dagger2可以通过自定义注解限定注解作用域。后面会演示一个例子,这是一个非常强大的特点,因为就如前面说的一样,没必要让每个对象都去了解如何管理他们的实例。

  介绍的差不多了,来看一个简单的实例,该实例参考了该项目和其相关的文章。该实例只是讲解怎么使用dagger2,并不涉及MVP,同时结合了当前流行的 Retrofit 2.0 、RxAndroid 等库(回想刚开始的时候做Android自己封装AsyncTask和使用BroadCast简直和原始人刀耕火种无异啊)。

  首先来看看整个工程的结构:

Dagger2 使用初步

  在 gradle 配置文件中首先引入需要的库:

apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt' // 注释处理


android {
    compileSdkVersion 23
    buildToolsVersion "23.0.2"

    defaultConfig {
        applicationId "com.zyp.archi.githubclient_mdr_0"
        minSdkVersion 16
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }

//    packagingOptions {
//        exclude 'META-INF/services/javax.annotation.processing.Processor'
//    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.1.1'

    compile 'com.android.support:recyclerview-v7:23.1.1' // RecyclerView

    compile 'com.jakewharton:butterknife:7.0.1' // 标注

    compile 'com.google.dagger:dagger:2.0.2' // dagger2
    compile 'com.google.dagger:dagger-compiler:2.0.2' // dagger2
    provided 'org.glassfish:javax.annotation:10.0-b28'

    compile 'io.reactivex:rxandroid:1.1.0' // RxAndroid
    compile 'io.reactivex:rxjava:1.1.0' // 推荐同时加载RxJava

    compile 'com.squareup.retrofit:retrofit:2.0.0-beta2' // Retrofit网络处理
    compile 'com.squareup.retrofit:adapter-rxjava:2.0.0-beta2' // Retrofit的rx解析库
    compile 'com.squareup.retrofit:converter-gson:2.0.0-beta2' // Retrofit的gson库

}

  由于 Dagger 使用 apt 生成代码,在Project gradle中还需要加入:

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.5.0'
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

  网络相关的API在Application中生成,注意别忘了在Manifest文件中添加 <uses-permission android:name=”android.permission.INTERNET”/> 和 绑定 android:name= .AppApplication 。

public class AppApplication extends Application{

    private static AppApplication sInstance;
    private AppComponent appComponent;

    @Override
    public void onCreate(){
        super.onCreate();
        this.sInstance = this;
        setupCompoent();
    }

    private void setupCompoent(){
        appComponent = DaggerAppComponent.builder()
                .githubApiModule(new GithubApiModule())
                .appModule(new AppModule(this))
                .build();
    }

    public static AppApplication getsInstance(){
        return sInstance;
    }

    public AppComponent getAppComponent(){
        return appComponent;
    }
}

  在 Application 中创建了 AppComponent 实例,并可以获取到,注意Appcomponent的实例化方式,Dagger + AppComponent.builder().somModule(new somModule()).build() ,注意写法,大小写也要注意,以后介绍Apt生成的原码的时候就清楚了为什么要这样写,我相信这里也是一个一开始不好理解的地方。接下来看看AppComponent是什么鬼。

@Component(modules = { AppModule.class, GithubApiModule.class})
public interface AppComponent {
    // inject what
    void inject(ReposListActivity activity);
}

  AppCompoent 是一个 Interface,通过 @Component 添加了两个 Module : AppModule、GithubApiModule。此外还有一个inject方法,其中的参数表示要注入的位置(先打个预防针,Component中的方法还可以起到暴露资源,实现Component中的“继承”的作用)。接下来看看AppModule 和 GithubApiModule。

@Module
public class AppModule {
    private Application application;

    public AppModule(Application application){
        this.application=application;
    }

    @Provides
    public Application provideApplication(){
        return application;
    }
}
@Module
public class GithubApiModule {

    @Provides
    public OkHttpClient provideOkHttpClient() {
        OkHttpClient okHttpClient = new OkHttpClient();
        okHttpClient.setConnectTimeout(60 * 1000, TimeUnit.MILLISECONDS);
        okHttpClient.setReadTimeout(60 * 1000, TimeUnit.MILLISECONDS);
        return okHttpClient;
    }

    @Provides
    public Retrofit provideRetrofit(Application application, OkHttpClient okHttpClient){
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(application.getString(R.string.api_github))
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) // 添加Rx适配器
                .addConverterFactory(GsonConverterFactory.create()) // 添加Gson转换器
                .client(okHttpClient)
                .build();
        return retrofit;
    }

    @Provides
    protected GithubApiService provideGitHubService(Retrofit retrofit) {

        return retrofit.create(GithubApiService.class);
    }
}

  这两个Module很简单,但是也包含很多东西。首先作为Module,需要使用@Module注解,在被@Module注解修饰的类内部,使用@Provides注解来表明可以提供的依赖对象。需要注意的是,有些由@Provides 提供的方法需要输入参数,这些参数是怎么来的呢?这对于刚刚接触的新手来说有点棘手。这里就先说了,这些需要传入的参数需要其它用@Provides注解修饰的方法生成,比如在GithubModule.class 中的 provideGitHubService(Retrofit retrofit) 方法中的参数就是由另外一个 @Provides 注解修饰的方法生成的,这里就是public Retrofit provideRetrofit(Application application, OkHttpClient okHttpClient),那么这个provideRetrofit()方法中的参数又是怎么来的呢?请读者自己去找。

  此外为什么GithubModule会这样设计,有没有更加单方法?试想当有多种 ApiService 需要用到的时候,OkhttpClient中的超时设置需要不同的时候,Retrofit 实例的 Converter需要不同的时候我们该如何应对?大家可以思考一下,我也在思考。

  这里使用到了Retrofit,Retrofit的基本使用方法见这里,虽然Retrofit2.0 还处于 beta 阶段,但是这里还是任性的使用了,Retrofit2.0 新特性和基本使用方式见这里

public interface GithubApiService {
    @GET("/users/{user}/repos")
    Observable<ArrayList<Repo>> getRepoData(@Path("user") String user);
}

  GithubAPiService 中定义了一个访问需要访问的接口,注意这里返回了一个Observable对象,这里使用到了 RxJava 的相关知识,RxJava的好处也很多,这里就不解释了,有兴趣入门参考见这里,此外建议大家参阅这里,其实都还不够。

  好了准备工作基本上做好了,现在来看看Activity怎么写。首先定义 BaseActivity,这里提一下基本的Android应用开发架构,稍微有经验的开发者肯定都是会对Activity 进行分层的,将一些公共的代码放在BaseActivity中,当然BaseActivity也许不止一种,或者不止一层,这就要具体问题具体分析了,此外一般还会引入utils 包来定义一些公共的静态方法来实现对这个应用的AOP,具体可以参考这里。这里BaseActivity 提供了ButterKnife依赖注入,提供了Component建立的方法和布局文件获取方法。

public abstract class BaseActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(getLayoutId());
        ButterKnife.bind(this);
        setupActivityComponent(AppApplication.getsInstance().getAppComponent());
    }

    protected abstract void setupActivityComponent(AppComponent appComponent);
    protected abstract int getLayoutId();

}

  这里设计了两个Activity,一个是MainActivity 一个是 ReposListActivity。MainActivity 提供一个按钮,点击则跳转到ReposListActivity,显示某一个人的GitHub账户上的信息。

public class MainActivity extends BaseActivity {

    @OnClick(R.id.showButton)
    public void onShowRepositoriesClick() {
        startActivity(new Intent(this, ReposListActivity.class));
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

    }

    @Override
    public int getLayoutId(){
        return R.layout.activity_main;
    }

    @Override
    public void setupActivityComponent(AppComponent appComponent){

    }
}
/**
 * Created by zhuyp on 2016/1/10.
 */
public class ReposListActivity extends BaseActivity {

    @Bind(R.id.repos_rv_list)
    RecyclerView mRvList;

    @Bind(R.id.pbLoading)
    ProgressBar pbLoading;


    @Inject
    GithubApiService githubApiService;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        initView();
    }

    @Override
    public int getLayoutId(){
        return R.layout.activity_repos_list;
    }

    @Override
    public void setupActivityComponent(AppComponent appComponent){
        appComponent.inject(this);
    }


    private void initView(){
        LinearLayoutManager manager = new LinearLayoutManager(this);
        manager.setOrientation(LinearLayoutManager.VERTICAL);
        mRvList.setLayoutManager(manager);

        ListAdapter adapter = new ListAdapter();
        mRvList.setAdapter(adapter);
        loadData(adapter);
    }

    private void loadData(final ListAdapter adapter){
        showLoading(true);
        githubApiService.getRepoData(getUser())
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new SimpleObserver<ArrayList<Repo>>() {
                    @Override
                    public void onNext(ArrayList<Repo> repos) {
                        showLoading(false);
                        adapter.setRepos(repos);
                    }
                    @Override
                    public void onError(Throwable e){
                        showLoading(false);
                    }
                });
    }

    private String getUser(){
        return "bird1015";
    }

    public void showLoading(boolean loading) {
        Log.i("info",loading + " Repos");
        pbLoading.setVisibility(loading ? View.VISIBLE : View.GONE);
    }
}

  简单说一下 ReposListActivity 中的依赖注入,@Inject 注入了GithubApiService,在loadData() 方法中读取对应用户GitHub上的信息并返回。这里我们只提取了很少一部分信息,并显示在RecyclerView中。由于本篇文章不是介绍Rxjava在Android中的应用,RxJava 相关就不做具体解释了,有兴趣可以从前面提到过的资料中去了解。从上面代码可以看到程序的处理逻辑异常清晰简单,这就是Rxjava的威力所在,但是这也是一个不好上手的东西,建议还是根据参考资料学习一下吧,不管能否实际运用,至少能看得懂啊。

  这只是Dagger2的一个入门实例代码,其实要搞懂Dagger需要看生成的源码(后面会写文章介绍),希望我能尽快再写一至两篇总结一下其它特性,比如 SubComponent ,Dependencies,Scope等。上面代码中还用到的资源我就直接贴在下面了。

public class ListAdapter extends RecyclerView.Adapter<ListAdapter.RepoViewHolder> {

    private ArrayList<Repo> mRepos;

    public ListAdapter() {
        mRepos = new ArrayList<>();
    }

    public void setRepos(ArrayList<Repo> repos) {
        mRepos = repos;
        notifyItemInserted(mRepos.size() - 1);
    }

    @Override
    public RepoViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item_repo, parent, false);
        return new RepoViewHolder(view);
    }

    @Override
    public void onBindViewHolder(RepoViewHolder holder, int position) {
        holder.bindTo(mRepos.get(position));
    }

    @Override
    public int getItemCount() {
        return mRepos.size();
    }

    public static class RepoViewHolder extends RecyclerView.ViewHolder {

        @Bind(R.id.item_iv_repo_name)
        TextView mIvRepoName;
        @Bind(R.id.item_iv_repo_detail)
        TextView mIvRepoDetail;

        public RepoViewHolder(View itemView) {
            super(itemView);
            ButterKnife.bind(this, itemView);
        }

        public void bindTo(Repo repo) {
            mIvRepoName.setText(repo.name );
            mIvRepoDetail.setText(String.valueOf(repo.description + "(" + repo.language + ")"));
        }
    }
}
/**
 * Created by zhuyp on 2016/1/10.
 */
public class Repo {
    public String name; // 库的名字
    public String description; // 描述
    public String language; // 语言
//  public String testNullField; // 试错
}
/**
 * Created by zhuyp on 2016/1/10.
 */
public class SimpleObserver<T> implements Observer<T> {
    @Override
    public void onCompleted() {

    }

    @Override
    public void onError(Throwable e) {

    }

    @Override
    public void onNext(T t) {

    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin">

    <Button
        android:id="@+id/showButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/main_goto_activity"/>

</LinearLayout>

activity_repo_list.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/repos_rv_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <ProgressBar
        android:id="@+id/pbLoading"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

item_repo.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              xmlns:tools="http://schemas.android.com/tools"
              android:orientation="vertical">

    <TextView
        android:id="@+id/item_iv_repo_name"
        tools:text="Repos name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="8dp"
        android:textColor="@android:color/holo_purple"
        android:textSize="22sp"/>

    <TextView
        android:id="@+id/item_iv_repo_detail"
        tools:text="Details"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="8dp"
        android:textSize="14sp"/>

</LinearLayout>

strings.xml

<resources>
    <string name="app_name">GithubClient_mdr_0</string>

    <string name="main_mock_data">自定义数据(测试)</string>
    <string name="main_goto_activity">跳转列表</string>
    <string name="api_github">https://api.github.com</string>

</resources>

 

Android Studio1.4.x JNI开发基础 – 简单实例

  接上一篇,搭建好基于Android Studio的环境之后,编写native代码相对来说也比较简单了。在Android上编写Native代码和在Linux编写C/C++代码还是有区别,Native代码一般需要与JVM交互数据,需要遵循一定的规范,本文来介绍一下基本的JNI代码写法。

  我们还是从实例出发,配置好Android Studio工程之后,我们需要创建jni目录和在jni目下创建c/c++文件和相应的头文件,创建方式见下图。

Android Studio1.4.x JNI开发基础 - 简单实例

Android Studio1.4.x JNI开发基础 - 简单实例

  在实例工程中我们创建了NdkSample.cpp 和 NdkSample.h,源码见下面:

#include "NdkSample.h"

JNIEXPORT jstring JNICALL Java_com_zyp_ndktest_MainActivity_sayHello
        (JNIEnv *env, jclass cls, jstring j_str)
{
    const char *c_str = nullptr;
    char buff[128] = {0};
    jboolean isCopy;    
    c_str = env->GetStringUTFChars(j_str, &isCopy);
    printf("isCopy:%d/n",isCopy);
    if(c_str == NULL)
    {
        return NULL;
    }
    printf("C_str: %s /n", c_str);
    sprintf(buff, "hello %s", c_str);
    env->ReleaseStringUTFChars(j_str, c_str);
    return env->NewStringUTF(buff);
}  
#ifndef NDKTEST_NDKSAMPLE_H
#define NDKTEST_NDKSAMPLE_H

#include "jni.h"
#include <stdio.h>
#include <string.h>

extern "C" {
JNIEXPORT jstring JNICALL
        Java_com_zyp_ndktest_MainActivity_sayHello(JNIEnv *env, jclass type,
                                                       jstring filename);
}

#endif //NDKTEST_NDKSAMPLE_H

  现在来简单介绍一下,首先是NdkSample.h文件,刚刚创建的时候只有相应的预处理命令,我们在头文件预处理命令之间加上 jni.h ,stdio.h ,string.h 后两个非必要。将我们要在java层调用的接口声明出来,放在extern “c”{} 中(告诉编译器按照C标准进行编译)。第一次接触jni的同学看到那么复杂的函数命名和奇怪的JNIEXPORT ,JNICALL,JNIEnv之类的估计有点不习惯,本文就不详细介绍它们的意思,其实你跟踪源码它们就是几个宏(JNIEnv是一个结构体保存当前环境的上下文),其它的jstring,jclass之类的很好理解就是在Native环境中对JVM中java对应结构的一种表示方式。

  函数命令方式是包名加activity名加函数名,表如我们在java层中的包名是java.com.zyp.ndktest,在MainActivity中调用sayHello函数,则jni层函数命名就要写成上面的方式。

  接下来看NdkSample.cpp文件中函数的定义。ni层的函数还需要多两个参数,一个是JNIEnv * ,一个是jclass。我们在java层调用的时候就只用传递前两个参数之外的参数。例子中我们想从java层传递一个String类型的参数到jni层,jni层从JVM中取数据的时候取到的却是jstring类型,在jni层我们不能直接使用需要转换。这里我们通过env->GetStringUTFChars(j_str, &isCopy)函数来完成,将j_str所在的地址转换并赋值给const char *类型的指针,之后我们就可以通过该指针访问那块内存了。注意这里是const char * 表示该指针指向的内存区域的内容是不可以改变的,java中的String 也是自带final属性的。GetStringUTFChars()实际上是将JVM内部的Unicode转化成为了C/C++认识的UTF-8的格式的字符串,注意这个函数内部发生了内存分配,相当于是拷贝了一份Unicode然后进行转化,所以后面需要ReleaseStringUTFChars()来释放内存。

  最后该函数返回一个新的构建好的jstring类型给java层,为了将C/C++层的UTF-8字符串转换为JVM中的Unicode字符串,需要调用另外一个函数NewStringUTF()来完成转换。

  我们注意到JVM中的内容jni中不能直接操作需要进行转换,jni中的内同也要进行转换,因此也有大量的相关jni接口存在,后面文章中会挑选一些来讲解。

  此外,函数中JNIEnv *env这个参数需要说一下,在C和C++中使用方式是不一样的,不要搞混了。在C中,看到JNIEnv 我们实质是取得了JNINativeInterface* (JNIEnv指针的指针),我们得使用**env获取结构体,从而才能使用结构体里面的方法。在C++中,看到JNIEnv我们实质是取得了JNIEnv*(JNIEnv结构体的指针),我们可以直接使用env->使用结构体里面的方法。注意我们调用GetStringUTFChars()的方式,但是注意和Java_com_zyp_ndktest_MainActivity_sayHello()进行区别。

  现在来看看我们在java层中如何调用jni层的接口,看下面代码:

package com.zyp.ndktest;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        String ret = sayHello("zhuzhu");
        Log.i("JNI_INFO", ret);
    }

    static {
        System.loadLibrary("NdkSample");
    }

    public native static String sayHello(String str);
}

  我们首先要通过System.loadLibrary()加载jni代码编译后生成的.so,但是这个库的名字怎么来的呢,注意回过头去看上一篇中gradle ndk{}中的内容,我们是在那里进行的命名的;然后还要声明native static 类型的该函数。然后直接调用就好了。运行结果见下图。

Android Studio1.4.x JNI开发基础 - 简单实例

  希望通过这篇文章能够让大家入门JNI开发^_^。

 

Android Studio1.4.x JNI开发基础-基本环境配置

  从Eclipse时代到Android Studio普及,开发工具越来越好用。早些时候还需要安装Cygwin工具,从Android Studio1.3以后,在Android 环境开发JNI程序搭建开发环境变得相对简单。这里就来介绍一下急于Android Studio如何进行jni开发。

  首先准备基本工具,Android Studio (>=1.3.x), NDK(ndk-r10-e)。打开Android Studio 建立一个空工程,关联上NDK,操作步骤方式如下图:

 

Android Studio1.4.x JNI开发基础-基本环境配置

 

Android Studio1.4.x JNI开发基础-基本环境配置

  设置好NDK之后,开始设置gradle,设置gradle主要需要设置三个地方,设置好之后就可以直接编写和编译JNI代码了,不需要像以前一样编写Makefile,相当方便。但是设置gradle也是需要比较小心的,由于当前NDK还处于Experimental 阶段,更新不断,经常会爆出各种奇怪的错误,因此也要特别留心。好了废话不多说,下面来介绍设置gradle的三个主要步骤。

  首先设置TopLevel gradle,也就是Project gradle,这里比较简单,在dependencies中设置:classpath ‘com.android.tools.build:gradle-experimental:0.2.0’ ,注意这里要把之前的类似classpath ‘com.android.tools.build:gradle:1.3.0’ 注释掉。还要多提一句的是,这里设置的是gradle-experimental:0.2.0,后面对应设置gradle wrapper的时候要对应gradle2.5-all 版本,这里先说到这里。

  接着设置 Module gradle,这一步是比较麻烦的。由于我们在创建工程的时候自动生成的这个gradle文件内容比较多,而且如果要使用NDK的话这个gradle变化比较大,这里直接贴出需要使用NDK的gradle,然后来进行说明。

  
apply plugin: 'com.android.model.application' model { android { compileSdkVersion = 23 buildToolsVersion = "23.0.2" defaultConfig.with { applicationId = "com.zyp.ndktest" minSdkVersion.apiLevel = 19 // Unable to load class com.android.build.gradle.managed.ProductFlavor_Impl targetSdkVersion.apiLevel = 23 versionCode = 1 versionName = "1.0" } } android.buildTypes { release { minifyEnabled = false proguardFiles += file('proguard-rules.pro') } } compileOptions.with { sourceCompatibility = JavaVersion.VERSION_1_7 targetCompatibility = JavaVersion.VERSION_1_7 } android.ndk { moduleName = "NdkSample" cppFlags += "-std=c++11" cppFlags += "-fexceptions" cppFlags += "-I${file("src/main/jni//include")}".toString() ldLibs += ["android", "log"] stl = "gnustl_shared" } android.productFlavors { create("arm7") { ndk.abiFilters.add("armeabi-v7a") } create("arm8") { ndk.abiFilters.add("arm64-v8a") } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:23.1.0' }

  和自动生成的gradle相比,首先是 apply plugin: ‘com.android.application’ 变成了 apply plugin: ‘com.android.model.application’。下面的配置也需要包装在model{}中。

  这个gradle的配置有几点需要注意的:

  1. 所有值的设置都要写成 xxx = yyyy的形式,比如: applicationId = “com.zyp.ndktest” (自动生成的gradle 则可能是: applicationId = “com.zyp.ndktest”  ),否则会爆这种错误:Error:Cause: org.gradle.api.internal.ExtensibleDynamicObject, 当出现此类错误,检查是否都用了 “=”的方式。

  2. buildTypes 需要从android{} 中取出来,写成android.buildTypes{}的形式,否则会出现这种错误:Error:Unable to load class ‘org.gradle.nativeplatform.internal.DefaultBuildType_Decorated’.    

   此外,自动生成的buildTypes的形式和上面的也不一样为以下的形式:

          
release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' }

  需要改成上面文件中的格式,否则会报这种错误:Error:No signature of method: org.gradle.model.ModelMap.minifyEnabled() is applicable for argument types: (java.lang.Boolean) values: [false]     

  3. defaultConfig{} 需要写成defaultConfig.with{} 的形式,否则会报这种错误:Error:Cause: com.android.build.gradle.managed.AndroidConfig_Impl

  4. 在defaultConfig.with{} 中 需要写成 

    minSdkVersion.apiLevel  = 19  
    targetSdkVersion.apiLevel = 23

    也就是比自动生成的多 .apiLevel ,否则会报这种错误:Unable to load class com.android.build.gradle.managed.ProductFlavor_Impl

  5. 增加compileOptions.with{} 需要选择JavaVersion.VERSION_1_7,否则会报这种错误:Bad class file magic or version

  6. 最后一点,在gradleWrapper中使用的是2.5,则android.ndk {} 中类似cppFlags 的添加使用 += 的方式,否则需要使用 .add的方式

  以上可能遇到的问题我这里帮大家罗列出来,具体的请参考Google的文档,只不过这个文档需要FQ。

  最后设置gradle wrapper就好了,将左边的工程视图调整到Project,在gradle->wrapper->grale-wrapper.properties文件的最后设置:distributionUrl=https/://services.gradle.org/distributions/gradle-2.5-all.zip,注意这里如果在Project gradle中设置的是gradle-experimental:0.2.0,则这里选择gradle-2.5-all,如果是gradle-experimental:0.4.0,需要设置gradle-2.8-all。

  gradle设置完成之后就可以创建jni文件夹,然后编写Native代码了,创建好jni后一个工程的基本结构见下图:

Android Studio1.4.x JNI开发基础-基本环境配置

  在JNI中创建.cpp/.c文件即可。本文先写到这里,接下来会介绍基本的jni实例和jni编程的及一些基本知识,希望对大家有帮助。