第2章 Android网络底层框架设计

本章介绍Android网络底层的封装。很多公司、很多团队都只是把网络底层封装成一个好用的方法,而我接下来要介绍的内容将覆盖的范围很广:

  • 抛弃AsyncTask,自定义一套网络底层的封装框架。
  • 设计一套App缓存策略。
  • 设计一套MockService的机制,在没有MobileAPI的时候,也能假装获取到了网络返回的数据。
  • 封装了用户Cookie的逻辑。

2.1 使用原生的ThreadPoolExecutor+Runnable+Handler

先说说AsyncTask的致命缺点:
那就是不能灵活控制其内部的线程池,线程池里面的每个线程存放的都是MobileAPI的调用请求,而AsyncTask中又没有暴露出取消这些请求的方法,也就是我们熟知的CancelRequest方法,所以,一旦从A页面跳转到B页面,那么在A页面发起的MobileAPI请求,如果还没有返回,并不会被取消。

图中只列出了8个,还有1个RemoteService类,位于YoungHeart项目的engine包中。下面分别介绍。

2.1.1 网络请求的格式

1. Request格式
2. Response格式

JSON数据格式1:

{  "code" : 1,    "message" : "网络异常",    "result" : ""}

可以定义code为0为成功,其他为异常情况。

3. UrlConfigManager和URLData

我们把App所要调用的所有MobileAPI接口的信息都放在url.xml文件中,如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<url>    
    <Node       
          Key="getWeatherInfo"        
          Expires="300"        
          NetType="get"         
          Url="http://www.weather.com.cn/data/sk/101010100.html" />     
    <Node       
          Key="login"        
          Expires="0"        
          NetType="post"        
          Url="http://www.weather.com.cn/data/login.api" />
</url>

在使用上,通过UrlConfigManager的findURL方法,在上述xml文件中找到当前MobileAPI调用的节点,其中每一个MobileAPI接口都对应一个URLData实体,如下所示:

public class URLData {
        private String key;
        private long expires;
        private String netType;
        private String url;
    }
4. RemoteService和RequestCallback、RequestParameter
  • RequestCallback是回调,目前有onSuccess和onFail两种。
  • RequestParameter是用来传递调用MobileAPI接口所需参数的键值对的。我们原本可以使用HashMap<String,String>这样的数据结构,但是HashMap比较耗费内存,虽然它的查找速度是o(1),而对于MobileAPI接口的参数而言,数据一般不会太多,查找速度快体现不出优势来,所以我们使用ArrayList<RequestParameter>这样的数据结构。
  • RemoteService这个单例是用来发起请求的,它会创建一个request,并将其添加到RequestManager中,然后放到DefaultThreadPool的一个线程中去执行这个request。
5. RequestManager

RequestManager这个集合类是用于取消请求(cancelRe-quest)的。因为每次发起请求,都会把为此创建的request添加到RequestManager中,所以RequestManager保存了全部re-quest。

6. DefaultThreadPool

DefaultThreadPool只是对ThreadPoolExecutor和Array-BlockingQueue的简单封装。我们可以认为它就是一个线程池,每发起一次请求(runnable),就由线程池分配一个新的线程来执行该请求。

7. HttpRequest

HttpRequest是发起Http请求的地方,它实现了Runnable,从而让DefaultThreadPool可以分配新的线程来执行它,所以,所有的请求逻辑都在Runnable接口的run方法中,其中:

  • 对于get形式的MobileAPI接口,它会把从上层传递进来的ArrayList<RequestParameter>,解析为urlk1=v1&k2=v2这样的形式。
  • 对于post格式的MobileAPI接口,它会把从上层传递进来的ArrayList<RequestParameter>,转为BasicNameVal-uePair的形式,放到表单中进行提交。

2.1.2 网络底层的一些优化工作

我们的网络底层越来越强大了,是否有意犹未尽的感受?接下来将完善这个框架,修复其中的一些瑕疵,如onFail的统一处理机制、UrlConfigManager的优化、ProgressBar的处理等。

1. onFail的统一处理机制

统一处理异常,Toast提示或者对话框

2. UrlConfigManager的优化

在App启动时,一次性将url.xml文件都读取到内存,把所有的UrlData实体保存在一个集合中,然后每次调用MobileAPI接口,直接从内存的这个集合中查找。考虑到内存中的数据会被回收,所以上述这个集合一旦为空,我们要从url.xml中再次读取。

3. 不是每个请求都需要回调的

2.2 App数据缓存设计

  • 对于App而言,它是感受不到取的是缓存数据还是调用MobileAPI。具体工作由网络底层完成。
  • 在url.xml中为每一个MobileAPI接口配置缓存时间Ex-pired。对于post,一律设置为0,因为post不需要缓存。
  • 在HttpRequest类中的run方法中,改动3个地方:
    • 写一个排序算法sortKeys,对URL中的key进行排序。
    • 将newUrl作为key,检查缓存中是否有数据,有则直接返回;否则,继续调用MobileAPI接口。
    • MobileAPI接口返回数据后,将数据存入缓存。
  • CacheManager用于操作读写缓存数据,并判断缓存数据是否过期。缓存中存放的实体就是CacheItem。
  • 在App项目中,创建YoungHeartApplication这个Ap-plication级别的类,在程序启动时,初始化缓存的目录,如果不存在则创建之。

2.3 强制更新

  • 如果对于某个接口的数据,MobileAPI缓存了5分钟,App缓存了3分钟,那么最极端的情况是,用户在8分钟内是看不到数据更新的。因此,我们需要在页面上提供一个强制更新的按钮。
  • 我们可以让RemoteService多暴露一个boolean类型的参数,用于判断是否要遵守App端缓存策略,如果是,则在从url.xml中取出UrlData实体后,将其expired强制设置为0,这样就不会执行缓存策略了。
  • 数据缓存是一把双刃剑,设置时间长了,数据长期不更新,用户体验就会不好。因此我们需要为那些强迫症类型的用户提供一个强制刷新的按钮,点击按钮后,页面会重新调用MobileAPI加载数据,无论缓存是否到期。

2.4 MockService

在App团队与MobileAPI团队协同开发的过程中,经常会遇到因为MobileAPI接口还没好而App又急等着用的情况。

设计App端MockService包括如下几个关键点:

  1. 对需要Mock数据的MobileAPI接口,通过在url.xml中配置Node节点MockClass属性,来指定要使用那个Mock子类生成的数据:
<Node    
  Key="getWeatherInfo"    
  Expires="300"    
  NetType="get"    
  MockClass="com.youngheart.mockdata.MockWeatherInfo"           
  Url="http://www.weather.com.cn/data/sk/101010100.html" />

这里将使用com.mockdata.mockdata包下的MockWeath-erInfo子类来解析。

  1. 我使用了反射工厂来设计MockService。MockService类是基类,它有一个抽象方法getJsonData,用于返回手动生成的Mock数据。
  2. 接下来介绍如何实现反射机制。
  • 主要的改造工作在RemoteService类的invoke方法中,根据是否在url.xml中指定了MockClass值来决定,是调用线上MobileAPI还是从本地MockService直接取假数据。
  • 如果MockClass有值,就把这个值反射为一个具体的类,比如MockWeatherInfo,然后调用它的getJsonData方法。
  • 有了MockService这个利器,对于作者来说,本书接下来的内容将会轻松很多,因为不需要搭建自己的服务器,全都用MockService在本地编写假数据即可。

2.5 用户登录

首先,贯穿App的,应该有一个User全局变量,在每次登录成功后,会将其isLogin属性设置为true,在退出登录后,则将该属性设置为false。这个User全局变量要支持序列化到本地的功能,这样数据才不会因内存回收而丢失。
其次,登录分为3种情形:

  1. 点击登录按钮,进入登录页面LoginActivity,登录成功后,直接进入个人中心PersonCenterActivity。这种情况最直截了当,一路执行startActivity(intent)就能达到目的。
  2. 在页面A,想要跳转到页面B,并携带一些参数,却发现没有登录,于是先跳转到登录页,登录成功后,再跳转到B页面,同时仍然带着那些参数。
  3. 在页面A,执行某个操作,却发现没有登录,于是跳转到登录页,登录成功后,再回到页面A,继续执行该操作。

这里,我的操作是在父类写一个startActivityForLogin,入参为intent

2.6 自动登录

  • 我们将cookie取出来,不用关心它是什么,只要把它存放在本地文件中即可。每次发起MobileAPI请求时,都要把本地保存的Cookie取出来,放到HttpRequest的header中。
  • SD卡以及内存中一份用户信息

判断用户是否过期需要服务器返回标识

2.7 HTTP头中的奥妙

对于HTTP头,我们并不陌生。我们在上一节中成功运用到了HTTP头中的Cookie属性。接下来,我们将继续发挥它的威力,看看它还能为我们做些什么。我们先学习一下HTTP请求的定义。

2.7.1 HTTP请求

HTTP请求分为HTTPRequest和HTTPResponse两种。但无论哪种请求,都由header和body两部分组成。

  1. HTTP Body:Body部分就是存放数据的地方
  2. HTTP Header:它由很多键值对(key-value)组成,其中有些key是标准的,兼容于各大浏览器,比如:
  • accept
  • accept-language
  • referrer
  • user-agent
  • accept-encoding
    我们还可以在MobileAPI端自定义一些键值对,然后要求App在调用MobileAPI时把这些信息传递过来。比如MobileAPI可以定义一个check-value这样的key,然后要求App将AppId(同一公司的不同App编号)、ClientType(Android还是iPhone、iPad)这些值拼接在一起经过MD5加密后,作为这个key的值传递给MobileAPI,然后由MobileAPI再去分析这些数据。
2.7.2 时间校准
  • 对于手机系统时间不准的问题,本文给出了比较好的解决方案,即通过每次调用MobileAPI来计算时间差,然后每次本地获取时间就加上这个时间差。
  • 对于用户身处不同时区的问题,App仍然返回同一个时间,只是要在App上注明这些时间都是北京时间,而不能是北京用户显示飞机9点起飞而日本用户显示10点起飞。另一方面,这两个时区的用户在一起聊天是个麻烦的事情,即使有人在日本时区10点说句话,对于北京用户而言,看到的也应该是9点发的消息,反之亦然。而服务器则要使用格林威治一套时间,具体怎么显示,那是App的事情。有些App就存在这样的bug,出国旅游收不到即时聊天消息,到了晚上会莫名其妙冒出来几百条消息,就是因为这个时区问题没有处理好导致的。
2.7.3 开启gzip压缩

接下来要介绍的内容和gzip有关。HTTP协议上的gzip编码是一种用来改进Web应用程序性能的技术。大流量的Web站点常常使用gzip压缩技术来减少传输量的大小,减少传输量大小有两个明显的好处,一是可以减少存储空间,二是通过网络传输时,可以减少传输的时间。

2.8 本章小结

本章介绍如何对网络底层进行封装,其中包括:新写了一个网络调用框架用以代替AsyncTask;设计了App的缓存机制;设计了MockService的机制,以后即使没有MobileAPI接口也能开发新功能了。介绍用户Cookie的设计方法;巧妙运用Http头中的数据等。

推荐阅读更多精彩内容