编辑推荐:稀土掘金,这是一个高质量的技术干货分享社区,web前端、Android、iOS、设计资源和产品,满足你的学习欲望。

原文出处:http://www.jianshu.com/p/3141d4e46240

网络请求是android客户端很重要的部分。下面从入门级开始介绍下自己Android网络请求的实践历程。希望能给刚接触Android网络部分的朋友一些帮助。
本文包含:

  • HTTP请求&响应

  • Get&Post

  • HttpClient & HttpURLConnection

  • 同步&异步

  • HTTP缓存机制

  • Volley&OkHttp

  • Retrofit&RestAPI

  • 网络图片加载优化

  • Fresco&Glide

  • 图片管理方案

HTTP请求&响应

既然说从入门级开始就说说Http请求包的结构。
一次请求就是向目标服务器发送一串文本。什么样的文本?有下面结构的文本。
HTTP请求包结构

例子:

1 2 3 4 5 6 7 POST/meme.php/home/user/loginHTTP/1.1 Host:114.215.86.90 Cache-Control:no-cache Postman-Token:bd243d6b-da03-902f-0a2c-8e9377f6f6ed Content-Type:application/x-www-form-urlencoded tel=13637829200&password=123456

请求了就会收到响应包(如果对面存在HTTP服务器)
HTTP响应包结构

例子:

1 2 3 4 5 6 7 8 9 10 HTTP/1.1200OK Date:Sat,02Jan201613:20:55GMT Server:Apache/2.4.6(CentOS)PHP/5.6.14 X-Powered-By:PHP/5.6.14 Content-Length:78 Keep-Alive:timeout=5,max=100 Connection:Keep-Alive Content-Type:application/json;charset=utf-8 { "status" :202, "info" : "\u6b64\u7528\u6237\u4e0d\u5b58\u5728\uff01" , "data" : null }

Http请求方式有

方法 描述
GET 请求指定url的数据,请求体为空(例如打开网页)。
POST 请求指定url的数据,同时传递参数(在请求体中)。
HEAD 类似于get请求,只不过返回的响应体为空,用于获取响应头。
PUT 从客户端向服务器传送的数据取代指定的文档的内容。
DELETE 请求服务器删除指定的页面。
CONNECT HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。
OPTIONS 允许客户端查看服务器的性能。
TRACE 回显服务器收到的请求,主要用于测试或诊断。

常用只有Post与Get。

Get&Post

网络请求中我们常用键值对来传输参数(少部分api用json来传递,毕竟不是主流)。
通过上面的介绍,可以看出虽然Post与Get本意一个是表单提交一个是请求页面,但本质并没有什么区别。下面说说参数在这2者的位置。

  • Get方式
    在url中填写参数:

    1 http: //xxxx.xx.com/xx.php?params1=value1&params2=value2

    甚至使用路由

    1 http: //xxxx.xx.com/xxx/value1/value2/value3

    这些就是web服务器框架的事了。

  • Post方式
    参数是经过编码放在请求体中的。编码包括x-www-form-urlencoded与form-data。
    x-www-form-urlencoded的编码方式是这样:

    1 tel=13637829200&password=123456

    form-data的编码方式是这样:

    1 2 3 4 5 6 7 8 9 ----WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition:form-data;name= "tel" 13637829200 ----WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition:form-data;name= "password" 123456 ----WebKitFormBoundary7MA4YWxkTrZu0gW

    x-www-form-urlencoded的优越性就很明显了。不过x-www-form-urlencoded只能传键值对,但是form-data可以传二进制

因为url是存在于请求行中的。
所以Get与Post区别本质就是参数是放在请求行中还是放在请求体中
当然无论用哪种都能放在请求头中。一般在请求头中放一些发送端的常量。

有人说:

  • Get是明文,Post隐藏
    移动端不是浏览器,不用https全都是明文。

  • Get传递数据上限XXX
    胡说。有限制的是浏览器中的url长度,不是Http协议,移动端请求无影响。

  • Get中文需要编码
    是真的...要注意。URLEncoder.encode(params, "gbk");

还是建议用post规范参数传递方式。并没有什么更优秀,只是大家都这样社会更和谐。

上面说的是请求。下面说响应。
请求是键值对,但返回数据我们常用Json。
对于内存中的结构数据,肯定要用数据描述语言将对象序列化成文本,再用Http传递,接收端并从文本还原成结构数据。
对象(服务器)<-->文本(Http传输)<-->对象(移动端) 。

服务器返回的数据大部分都是复杂的结构数据,所以Json最适合。
Json解析库有很多Google的Gson,阿里的FastJson。
Gson的用法看这里。


HttpClient & HttpURLConnection

HttpClient早被废弃了,谁更好这种问题也只有经验落后的面试官才会问。具体原因可以看这里。

下面说说HttpURLConnection的用法。
最开始接触的就是这个。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 publicclassNetUtils{ publicstaticStringpost(Stringurl,Stringcontent){ HttpURLConnectionconn= null ; try { //创建一个URL对象 URLmURL= new URL(url); //调用URL的openConnection()方法,获取HttpURLConnection对象 conn=(HttpURLConnection)mURL.openConnection(); conn.setRequestMethod( "POST" ); //设置请求方法为post conn.setReadTimeout(5000); //设置读取超时为5秒 conn.setConnectTimeout(10000); //设置连接网络超时为10秒 conn.setDoOutput( true ); //设置此方法,允许向服务器输出内容 //post请求的参数 Stringdata=content; //获得一个输出流,向服务器写数据,默认情况下,系统不允许向服务器输出内容 OutputStreamout=conn.getOutputStream(); //获得一个输出流,向服务器写数据 out.write(data.getBytes()); out.flush(); out.close(); intresponseCode=conn.getResponseCode(); //调用此方法就不必再使用conn.connect()方法 if (responseCode==200){ InputStreamis=conn.getInputStream(); Stringresponse=getStringFromInputStream(is); return response; } else { throw new NetworkErrorException( "responsestatusis" +responseCode); } } catch (Exceptione){ e.printStackTrace(); }finally{ if (conn!= null ){ conn.disconnect(); //关闭连接 } } return null ; } publicstaticStringget(Stringurl){ HttpURLConnectionconn= null ; try { //利用stringurl构建URL对象 URLmURL= new URL(url); conn=(HttpURLConnection)mURL.openConnection(); conn.setRequestMethod( "GET" ); conn.setReadTimeout(5000); conn.setConnectTimeout(10000); intresponseCode=conn.getResponseCode(); if (responseCode==200){ InputStreamis=conn.getInputStream(); Stringresponse=getStringFromInputStream(is); return response; } else { throw new NetworkErrorException( "responsestatusis" +responseCode); } } catch (Exceptione){ e.printStackTrace(); }finally{ if (conn!= null ){ conn.disconnect(); } } return null ; } privatestaticStringgetStringFromInputStream(InputStreamis) throwsIOException{ ByteArrayOutputStreamos= new ByteArrayOutputStream(); //模板代码必须熟练 byte[]buffer= new byte[1024]; intlen=-1; while ((len=is.read(buffer))!=-1){ os.write(buffer,0,len); } is.close(); Stringstate=os.toString(); //把流中的数据转换成字符串,采用的编码是utf-8(模拟器默认编码) os.close(); return state; } }

注意网络权限!被坑了多少次。

1 <uses-permissionandroid:name= "android.permission.INTERNET" />

同步&异步

这2个概念仅存在于多线程编程中。
android中默认只有一个主线程,也叫UI线程。因为View绘制只能在这个线程内进行。
所以如果你阻塞了(某些操作使这个线程在此处运行了N秒)这个线程,这期间View绘制将不能进行,UI就会卡。所以要极力避免在UI线程进行耗时操作。
网络请求是一个典型耗时操作。
通过上面的Utils类进行网络请求只有一行代码。

1 NetUtils.get( "http://www.baidu.com" );//这行代码将执行几百毫秒。

如果你这样写

1 2 3 4 5 6 @Override protectedvoidonCreate(BundlesavedInstanceState){ super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); Stringresponse=Utils.get( "http://www.baidu.com" ); }

就会死。。
这就是同步方式。直接耗时操作阻塞线程直到数据接收完毕然后返回。Android不允许的。
异步方式:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 //在主线程new的Handler,就会在主线程进行后续处理。 privateHandlerhandler= new Handler(); privateTextViewtextView; @Override protectedvoidonCreate(BundlesavedInstanceState){ super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView=(TextView)findViewById(R.id.text); new Thread( new Runnable(){ @Override publicvoidrun(){ //从网络获取数据 finalStringresponse=NetUtils.get( "http://www.baidu.com" ); //向Handler发送处理操作 handler.post( new Runnable(){ @Override publicvoidrun(){ //在UI线程更新UI textView.setText(response); } }); } }).start(); }

在子线程进行耗时操作,完成后通过Handler将更新UI的操作发送到主线程执行。这就叫异步。Handler是一个Android线程模型中重要的东西,与网络无关便不说了。关于Handler不了解就先去Google一下。
关于Handler原理一篇不错的文章

但这样写好难看。异步通常伴随者他的好基友回调。
这是通过回调封装的Utils类。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 publicclassAsynNetUtils{ publicinterfaceCallback{ voidonResponse(Stringresponse); } publicstaticvoidget(finalStringurl,finalCallbackcallback){ finalHandlerhandler= new Handler(); new Thread( new Runnable(){ @Override publicvoidrun(){ finalStringresponse=NetUtils.get(url); handler.post( new Runnable(){ @Override publicvoidrun(){ callback.onResponse(response); } }); } }); } publicstaticvoidpost(finalStringurl,finalStringcontent,finalCallbackcallback){ finalHandlerhandler= new Handler(); new Thread( new Runnable(){ @Override publicvoidrun(){ finalStringresponse=NetUtils.post(url,content); handler.post( new Runnable(){ @Override publicvoidrun(){ callback.onResponse(response); } }); } }); } }

然后使用方法。

1 2 3 4 5 6 7 8 9 10 11 12 privateTextViewtextView; @Override protectedvoidonCreate(BundlesavedInstanceState){ super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView=(TextView)findViewById(R.id.webview); AsynNetUtils.get( "http://www.baidu.com" , new AsynNetUtils.Callback(){ @Override publicvoidonResponse(Stringresponse){ textView.setText(response); } });

是不是优雅很多。
嗯,一个蠢到哭的网络请求方案成型了。
愚蠢的地方有很多:

  • 每次都new Thread,new Handler消耗过大

  • 没有异常处理机制

  • 没有缓存机制

  • 没有完善的API(请求头,参数,编码,拦截器等)与调试模式

  • 没有Https

HTTP缓存机制

缓存对于移动端是非常重要的存在。

  • 减少请求次数,减小服务器压力.

  • 本地数据读取速度更快,让页面不会空白几百毫秒。

  • 在无网络的情况下提供数据。

缓存一般由服务器控制(通过某些方式可以本地控制缓存,比如向过滤器添加缓存控制信息)。通过在请求头添加下面几个字端:

Request

请求头字段 意义
If-Modified-Since: Sun, 03 Jan 2016 03:47:16 GMT 缓存文件的最后修改时间。
If-None-Match: "3415g77s19tc3:0" 缓存文件的Etag(Hash)值
Cache-Control: no-cache 不使用缓存
Pragma: no-cache 不使用缓存

Response

响应头字段 意义
Cache-Control: public 响应被共有缓存,移动端无用
Cache-Control: private 响应被私有缓存,移动端无用
Cache-Control:no-cache 不缓存
Cache-Control:no-store 不缓存
Cache-Control: max-age=60 60秒之后缓存过期(相对时间)
Date: Sun, 03 Jan 2016 04:07:01 GMT 当前response发送的时间
Expires: Sun, 03 Jan 2016 07:07:01 GMT 缓存过期的时间(绝对时间)
Last-Modified: Sun, 03 Jan 2016 04:07:01 GMT 服务器端文件的最后修改时间
ETag: "3415g77s19tc3:0" 服务器端文件的Etag[Hash]值

正式使用时按需求也许只包含其中部分字段。
客户端要根据这些信息储存这次请求信息。
然后在客户端发起请求的时候要检查缓存。遵循下面步骤:

注意服务器返回304意思是数据没有变动滚去读缓存信息。
曾经年轻的我为自己写的网络请求框架添加完善了缓存机制,还沾沾自喜,直到有一天我看到了下面2个东西。(/TДT)/

Volley&OkHttp

Volley&OkHttp应该是现在最常用的网络请求库。用法也非常相似。都是用构造请求加入请求队列的方式管理网络请求。

先说Volley:
Volley可以通过这个库进行依赖.
Volley在Android 2.3及以上版本,使用的是HttpURLConnection,而在Android 2.2及以下版本,使用的是HttpClient。
Volley的基本用法,网上资料无数,这里推荐郭霖大神的博客
Volley存在一个缓存线程,一个网络请求线程池(默认4个线程)。
Volley这样直接用开发效率会比较低,我将我使用Volley时的各种技巧封装成了一个库RequestVolly.
我在这个库中将构造请求的方式封装为了函数式调用。维持一个全局的请求队列,拓展一些方便的API。

不过再怎么封装Volley在功能拓展性上始终无法与OkHttp相比。
Volley停止了更新,而OkHttp得到了官方的认可,并在不断优化。
因此我最终替换为了OkHttp

OkHttp用法见这里
很友好的API与详尽的文档。
这篇文章也写的很详细了。
OkHttp使用Okio进行数据传输。都是Square家的。
但并不是直接用OkHttp。Square公司还出了一个Retrofit库配合OkHttp战斗力翻倍。

Retrofit&RestAPI

Retrofit极大的简化了网络请求的操作,它应该说只是一个Rest API管理库,它是直接使用OKHttp进行网络请求并不影响你对OkHttp进行配置。毕竟都是Square公司出品。
RestAPI是一种软件设计风格。
服务器作为资源存放地。客户端去请求GET,PUT, POST,DELETE资源。并且是无状态的,没有session的参与。
移动端与服务器交互最重要的就是API的设计。比如这是一个标准的登录接口。

你们应该看的出这个接口对应的请求包与响应包大概是什么样子吧。
请求方式,请求参数,响应数据,都很清晰。
使用Retrofit这些API可以直观的体现在代码中。

然后使用Retrofit提供给你的这个接口的实现类 就能直接进行网络请求获得结构数据。

注意Retrofit2.0相较1.9进行了大量不兼容更新。google上大部分教程都是基于1.9的。这里有个2.0的教程。

教程里进行异步请求是使用Call。Retrofit最强大的地方在于支持RxJava。就像我上图中返回的是一个Observable。RxJava上手难度比较高,但用过就再也离不开了。Retrofit+OkHttp+RxJava配合框架打出成吨的输出,这里不再多说。

网络请求学习到这里我觉得已经到顶了。。

网络图片加载优化

对于图片的传输,就像上面的登录接口的avatar字段,并不会直接把图片写在返回内容里,而是给一个图片的地址。需要时再去加载。

如果你直接用HttpURLConnection去取一张图片,你办得到,不过没优化就只是个BUG不断demo。绝对不能正式使用。
注意网络图片有些特点:

  1. 它永远不会变
    一个链接对应的图片一般永远不会变,所以当第一次加载了图片时,就应该予以永久缓存,以后就不再网络请求。

  2. 它很占内存
    一张图片小的几十k多的几M高清无码。尺寸也是64*64到2k图。你不能就这样直接显示到UI,甚至不能直接放进内存。

  3. 它要加载很久
    加载一张图片需要几百ms到几m。这期间的UI占位图功能也是必须考虑的。

说说我在上面提到的RequestVolley里做的图片请求处理(没错我做了,这部分的代码可以去github里看源码)。

三级缓存

网上常说三级缓存--服务器,文件,内存。不过我觉得服务器不算是一级缓存,那就是数据源嘛。

  • 内存缓存
    首先内存缓存使用LruCache。LRU是Least Recently Used 近期最少使用算法,这里确定一个大小,当Map里对象大小总和大于这个大小时将使用频率最低的对象释放。我将内存大小限制为进程可用内存的1/8.
    内存缓存里读得到的数据就直接返回,读不到的向硬盘缓存要数据。

  • 硬盘缓存
    硬盘缓存使用DiskLruCache。这个类不在API中。得复制使用。
    看见LRU就明白了吧。我将硬盘缓存大小设置为100M。

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Override publicvoidputBitmap(Stringurl,Bitmapbitmap){ put(url,bitmap); //向内存Lru缓存存放数据时,主动放进硬盘缓存里 try { Editoreditor=mDiskLruCache.edit(hashKeyForDisk(url)); bitmap.compress(Bitmap.CompressFormat.JPEG,100,editor.newOutputStream(0)); editor.commit(); } catch (IOExceptione){ e.printStackTrace(); } } //当内存Lru缓存中没有所需数据时,调用创造。 @Override protectedBitmapcreate(Stringurl){ //获取key Stringkey=hashKeyForDisk(url); //从硬盘读取数据 Bitmapbitmap= null ; try { DiskLruCache.SnapshotsnapShot=mDiskLruCache.get(key); if (snapShot!= null ){ bitmap=BitmapFactory.decodeStream(snapShot.getInputStream(0)); } } catch (IOExceptione){ e.printStackTrace(); } return bitmap; }

    DiskLruCache的原理不再解释了(我还解决了它存在的一个BUG,向Log中添加的数据增删记录时,最后一条没有输出,导致最后一条缓存一直失效。)

  • 硬盘缓存也没有数据就返回空,然后就向服务器请求数据。

这就是整个流程。
但我这样的处理方案还是有很多局限。

  • 图片未经压缩处理直接存储使用

  • 文件操作在主线程

  • 没有完善的图片处理API

以前也觉得这样已经足够好直到我遇到下面俩。

Fresco&Glide

不用想也知道它们都做了非常完善的优化,重复造轮子的行为很蠢。
Fresco是Facebook公司的黑科技。光看功能介绍就看出非常强大。使用方法官方博客说的够详细了。
真三级缓存,变换后的BItmap(内存),变换前的原始图片(内存),硬盘缓存。
在内存管理上做到了极致。对于重度图片使用的APP应该是非常好的。
它一般是直接使用SimpleDraweeView来替换ImageView,呃~侵入性较强,依赖上它apk包直接大1M。代码量惊人。

所以我更喜欢Glide,作者是bumptech。这个库被广泛的运用在google的开源项目中,包括2014年google I/O大会上发布的官方app。
这里有详细介绍。直接使用ImageView即可,无需初始化,极简的API,丰富的拓展,链式调用都是我喜欢的。
丰富的拓展指的就是这个。
另外我也用过Picasso。API与Glide简直一模一样,功能略少,且有半年未修复的BUG。

图片管理方案

再说说图片存储。不要存在自己服务器上面,徒增流量压力,还没有图片处理功能。
推荐七牛与阿里云存储(没用过其它 π__π )。它们都有很重要的一项图片处理。在图片Url上加上参数来对图片进行一些处理再传输。
于是(七牛的处理代码)

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 publicstaticStringgetSmallImage(Stringimage){ if (image== null ) return null ; if (isQiniuAddress(image))image+= "?imageView2/0/w/" +IMAGE_SIZE_SMALL; return image; } publicstaticStringgetLargeImage(Stringimage){ if (image== null ) return null ; if (isQiniuAddress(image))image+= "?imageView2/0/w/" +IMAGE_SIZE_LARGE; return image; } publicstaticStringgetSizeImage(Stringimage,intwidth){ if (image== null ) return null ; if (isQiniuAddress(image))image+= "?imageView2/0/w/" +width; return image; }

既可以加快请求速度,又能减少流量。再配合Fresco或Glide。完美的图片加载方案。
不过这就需要你把所有图片都存放在七牛或阿里云,这样也不错。

图片/文件上传也都是使用它们第三方存储,它们都有SDK与官方文档教你。
不过图片一定要压缩过后上传。上传1-2M大的高清照片没意义。


更多相关文章

  1. mybatisplus的坑 insert标签insert into select无参数问题的解决
  2. python起点网月票榜字体反爬案例
  3. 细谈Android应用架构
  4. android app请求获取root权限
  5. Android中的Handler总结
  6. Android(安卓)操作SQLite数据库(初步)-在程序中删除数据库
  7. 【译】使用Kotlin从零开始写一个现代Android(安卓)项目-Part2
  8. Android(安卓)ContentProvider实例详解
  9. 我的android 第14天 - 使用SQLiteDatabase操作SQLite数据库

随机推荐

  1. Android(安卓)API 中文 (50) —— SpinnerA
  2. Android(安卓)gallery实现图片的左右循环
  3. Android(安卓)利用getIdentifier()方法获
  4. Android开发者指南(10) —— Android(安
  5. Android(安卓)MapView 申请apiKey
  6. Android(安卓)图片加水印
  7. 预显示TextView
  8. Android的一些开源项目集锦 以备以后研究
  9. Android游戏开发实践指南(华章程序员书库
  10. androidTV 9.0 开发调用系统jar异常报错,j