微信号:appjiagou

介绍:分享最有价值的APP技术干货文章,做一个有逼格的APP架构师,拒绝平庸,打造最有价值的APP社区!

Android网络请求心路历程(下)

2017-01-04 00:19 APP架构师


来源:Jude95  

链接:http://www.jianshu.com/p/d9e4ddd1c530

(接上文)


异步方式:


 //在主线程new的Handler,就会在主线程进行后续处理。

   private Handler handler = new Handler();

   private TextView textView;

   @Override

   protected void onCreate(Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);

       setContentView(R.layout.activity_main);

       textView = (TextView) findViewById(R.id.text);

       new Thread(new Runnable() {

           @Override

           public void run() {

                   //从网络获取数据

               final String response = NetUtils.get("http://www.baidu.com");

                   //向Handler发送处理操作

               handler.post(new Runnable() {

                   @Override

                   public void run() {

                           //在UI线程更新UI

                       textView.setText(response);

                   }

               });

           }

       }).start();

   }


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

关于Handler原理一篇不错的文章

http://www.cnblogs.com/codingmyworld/archive/2011/09/12/2174255.html


但这样写好难看。异步通常伴随者他的好基友回调。

这是通过回调封装的Utils类。


public class AsynNetUtils {

       public interface Callback{

           void onResponse(String response);

       }

       public static void get(final String url, final Callback callback){

           final Handler handler = new Handler();

           new Thread(new Runnable() {

               @Override

               public void run() {

                   final String response = NetUtils.get(url);

                   handler.post(new Runnable() {

                       @Override

                       public void run() {

                           callback.onResponse(response);

                       }

                   });

               }

           });

       }

       public static void post(final String url, final String content, final Callback callback){

           final Handler handler = new Handler();

           new Thread(new Runnable() {

               @Override

               public void run() {

                   final String response = NetUtils.post(url,content);

                   handler.post(new Runnable() {

                       @Override

                       public void run() {

                           callback.onResponse(response);

                       }

                   });

               }

           });

       }

   }


然后使用方法。


private TextView textView;

   @Override

   protected void onCreate(Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);

       setContentView(R.layout.activity_main);

       textView = (TextView) findViewById(R.id.webview);

       AsynNetUtils.get("http://www.baidu.com", new AsynNetUtils.Callback() {

           @Override

           public void onResponse(String response) {

               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用法见这里

http://square.github.io/okhttp/

很友好的API与详尽的文档。

这篇文章也写的很详细了。

http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0106/2275.htm

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。


@Override

 public void putBitmap(String url, Bitmap bitmap) {

     put(url, bitmap);

     //向内存Lru缓存存放数据时,主动放进硬盘缓存里

     try {

         Editor editor = mDiskLruCache.edit(hashKeyForDisk(url));

         bitmap.compress(Bitmap.CompressFormat.JPEG, 100, editor.newOutputStream(0));

         editor.commit();

     } catch (IOException e) {

         e.printStackTrace();

     }

 }

 //当内存Lru缓存中没有所需数据时,调用创造。

 @Override

 protected Bitmap create(String url) {

     //获取key

     String key = hashKeyForDisk(url);

     //从硬盘读取数据

     Bitmap bitmap = null;

     try {

         DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);

         if(snapShot!=null){

             bitmap = BitmapFactory.decodeStream(snapShot.getInputStream(0));

         }

     } catch (IOException e) {

         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,丰富的拓展,链式调用都是我喜欢的。

http://www.jianshu.com/p/4a3177b57949


丰富的拓展指的就是这个。

https://github.com/wasabeef/glide-transformations

另外我也用过Picasso。API与Glide简直一模一样,功能略少,且有半年未修复的BUG。


图片管理方案


再说说图片存储。不要存在自己服务器上面,徒增流量压力,还没有图片处理功能。


推荐七牛与阿里云存储(没用过其它 π__π )。它们都有很重要的一项图片处理。在图片Url上加上参数来对图片进行一些处理再传输。

于是(七牛的处理代码)


public static String getSmallImage(String image){

       if (image==null)return null;

       if (isQiniuAddress(image)) image+="?imageView2/0/w/"+IMAGE_SIZE_SMALL;

       return image;

   }

   public static String getLargeImage(String image){

       if (image==null)return null;

       if (isQiniuAddress(image)) image+="?imageView2/0/w/"+IMAGE_SIZE_LARGE;

       return image;

   }

   public static String getSizeImage(String image,int width){

       if (image==null)return null;

       if (isQiniuAddress(image)) image+="?imageView2/0/w/"+width;

       return image;

   }


既可以加快请求速度,又能减少流量。再配合Fresco或Glide。完美的图片加载方案。

不过这就需要你把所有图片都存放在七牛或阿里云,这样也不错。


图片/文件上传也都是使用它们第三方存储,它们都有SDK与官方文档教你。

不过图片一定要压缩过后上传。上传1-2M大的高清照片没意义。


 
APP架构师 更多文章 Android TextureView的原理分析_haihengcao_新浪博客 Android属性动画源代码解析(超详细) Android DiskLruCache 源码解析 硬盘缓存的绝佳方案(上) Android 沉浸式状态栏攻略 让你的状态栏变色吧 Android干货框架集锦,搭建项目必不可少
猜您喜欢 欢迎来到蘑菇街测试技术的微信公众号 尝试"分答" 支付宝又出一逆天神器,仅支持部分手机型号,小伙伴们赶紧去看看 《函数式 Swift》出版 国内IaaS云落地的桎梏(一):为什么企业应该拥抱以应用为中心的云管理