第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头中的数据等。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,087评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,544评论 25 707
  • 同样是使用Java语言,为什么做MobileAPI的开发人员写不了Android程序,反之亦然。我想大概是各行有各...
    lookid阅读 759评论 1 2
  • 湿鞋趟过那断桥的河 鱼儿划过我的脚边 想要知道河的尽头 沿途的荷花 偶遇的青蛙 潜伏着的半翅目 河边洗衣的大娘们 ...
    柒禾頁阅读 216评论 8 2
  • 增一阿含经卷三十四载,劫初之时,光音天子至此世间食地肥,食少者,其体不重,且不失神足,故亦能于虚空飞行;然食多...
    山海花开阅读 355评论 0 0